Repository: Codeception/CodeceptJS
Pull request #1986 (2019-10-22 14:23:18)

Introduce scrollIntoView for WebDriver & Appium
See https://webdriver.io/docs/api/element/scrollIntoView.html

Vorobeyko: Can you generate docs and typing please? 
Run:
```sh
npm run docs
npm run def
```
pablopaul: @Vorobeyko docs are added, for `npm run def` I get the following error:

```
npm run def

> codeceptjs@2.3.5 def /Users/paul/projects/CodeceptJS
> jsdoc -c typings/jsdoc.conf.js

[TSD-JSDoc] Failed to find parent of doclet 'module.exports' using memberof 'module', this is likely due to invalid JSDoc.
[TSD-JSDoc] Failed to find parent of doclet 'container.mocha' using memberof 'container', this is likely due to invalid JSDoc.
[TSD-JSDoc] Failed to find parent of doclet 'module.exports' using memberof 'module', this is likely due to invalid JSDoc.
[TSD-JSDoc] Failed to find parent of doclet 'context.Scenario' using memberof 'context', this is likely due to invalid JSDoc.
[TSD-JSDoc] Unable to resolve type for an unnamed item, this is likely due to invalid JSDoc. Often this is caused by invalid JSDoc on a parameter. Defaulting to `any`.
[TSD-JSDoc] Unable to resolve type for an unnamed item, this is likely due to invalid JSDoc. Often this is caused by invalid JSDoc on a parameter. Defaulting to `any`.
[TSD-JSDoc] Unable to resolve type for an unnamed item, this is likely due to invalid JSDoc. Often this is caused by invalid JSDoc on a parameter. Defaulting to `any`.
```
DavertMik: I approved. The next person who approves this should merge it
pablopaul: @PeterNgTr @DavertMik I rebased on latest master to resolve typings merge conflict, therefor I need your approval again.

Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index a1b14fe9..47969b41 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -120,6 +120,43 @@ program.command('run [test]')
 
   .action(require('../lib/command/run'));
 
+program.command('run-rerun [test]')
+  .description('Executes tests in more than one test suite run')
+
+  // codecept-only options
+  .option('--steps', 'show step-by-step execution')
+  .option('--debug', 'output additional information')
+  .option('--verbose', 'output internal logging information')
+  .option('-o, --override [value]', 'override current config options')
+  .option('--profile [value]', 'configuration profile to be used')
+  .option('-c, --config [file]', 'configuration file to be used')
+  .option('--features', 'run only *.feature files and skip tests')
+  .option('--tests', 'run only JS test files and skip features')
+  .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
+
+  // mocha options
+  .option('--colors', 'force enabling of colors')
+  .option('--no-colors', 'force disabling of colors')
+  .option('-G, --growl', 'enable growl notification support')
+  .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
+  .option('-R, --reporter <name>', 'specify the reporter to use')
+  .option('-S, --sort', 'sort test files')
+  .option('-b, --bail', 'bail after first test failure')
+  .option('-d, --debug', "enable node's debugger, synonym for node --debug")
+  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
+  .option('-f, --fgrep <string>', 'only run tests containing <string>')
+  .option('-i, --invert', 'inverts --grep and --fgrep matches')
+  .option('--full-trace', 'display the full stack trace')
+  .option('--compilers <ext>:<module>,...', 'use the given module(s) to compile files')
+  .option('--debug-brk', "enable node's debugger breaking on the first line")
+  .option('--inline-diffs', 'display actual/expected differences inline within each string')
+  .option('--no-exit', 'require a clean shutdown of the event loop: mocha will not call process.exit')
+  .option('--recursive', 'include sub directories')
+  .option('--trace', 'trace function calls')
+  .option('--child <string>', 'option for child processes')
+
+  .action(require('../lib/command/run-rerun'));
+
 program.command('run-workers <workers>')
   .description('Executes tests in workers')
   .option('-c, --config [file]', 'configuration file to be used')
diff --git a/docs/basics.md b/docs/basics.md
index 823188fe..10681ba6 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -755,3 +755,35 @@ Like in Mocha you can use `x` and `only` to skip tests or to run a single test.
 
 * `xScenario` - skips current test
 * `Scenario.only` - executes only the current test
+
+## Todo test
+
+You can use `Scenario.todo` when you are planning on writing tests. This test will be skipped like with usual `skip`.
+But this method to adds skip-message: `Test not implemented!` to test instance.
+
+Example #1 - with callback:
+```js
+Scenario.todo('Test',  I => {
+/**
+ * 1. Click to field
+ * 2. Fill field
+ * 
+ * Result:
+ * 3. Field contains text
+ */
+})
+```
+
+Example #2 - without callback:
+```js
+Scenario.todo('Test')
+```
+
+And you can access the `message` of `test.opts.skipInfo` in the events
+
+Example #3 - access into event hooks
+```js
+  event.dispatcher.on(event.test.before, (test) => {
+    test.opts.skipInfo //  contains {message and description}
+  });
+```
\ No newline at end of file
diff --git a/docs/commands.md b/docs/commands.md
index f9c01ea3..a8653e15 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -88,6 +88,27 @@ Run tests in parallel threads.
 npx codeceptjs run-workers 3
 ```
 
+## Run Rerun
+
+Run tests with rerun of all suite some cycles. Use config.rerun params:
+
+```
+{
+...
+rerun: {
+  //how many times all tests in suite must pass 
+    minSuccess: 2,
+  //how many times we can try to rerun all test suite for reaching minSuccess count of passed test suite  
+    maxReruns: 4,
+  }
+}
+```
+
+For example: 
+ - minSuccess 1, maxReruns 5 - CodeceptJS will run all test suite no more than 5 times, until first successful run
+ - minSuccess 3, maxReruns 5 - CodeceptJS will run all test suite no more than 5 times, until reaching 3 successfull runs
+ - minSuccess 10, maxReruns 10 - CodeceptJS will run all test suite 10 times, and if any one test in any run will fail - all suite is failed.
+
 ## Dry Run
 
 Prints test scenarios without executing them
@@ -205,4 +226,4 @@ Prints debugging information concerning the local environment
 
 ```sh
 npx codeceptjs info
-```
\ No newline at end of file
+```
diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md
index dee0556d..51cbef9f 100644
--- a/docs/helpers/Appium.md
+++ b/docs/helpers/Appium.md
@@ -579,6 +579,22 @@ I.runOnAndroid((caps) => {
 -   `caps` **any** 
 -   `fn` **any** 
 
+### scrollIntoView
+
+Scroll element into viewport.
+
+```js
+I.scrollIntoView('#submit');
+I.scrollIntoView('#submit', true);
+I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" });
+```
+
+#### Parameters
+
+-   `locator` **([string][4] | [object][5])** located by CSS|XPath|strict locator.
+-   `scrollIntoViewOptions`  
+-   `alignToTop` **([boolean][11] | [object][5])** (optional) or scrollIntoViewOptions (optional), see [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][12].Supported only for web testing
+
 ### see
 
 Checks that a page contains a visible text.
@@ -732,12 +748,12 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']);
 #### Parameters
 
 -   `select` **([string][4] | [object][5])** field located by label|name|CSS|XPath|strict locator.
--   `option` **([string][4] | [Array][7]&lt;any>)** visible text or value of option.-   Supported on only for web testing!
+-   `option` **([string][4] | [Array][7]&lt;any>)** visible text or value of option.Supported only for web testing
 
 ### sendDeviceKeyEvent
 
 Send a key event to the device.
-List of keys: [https://developer.android.com/reference/android/view/KeyEvent.html][11]
+List of keys: [https://developer.android.com/reference/android/view/KeyEvent.html][13]
 
 ```js
 I.sendDeviceKeyEvent(3);
@@ -751,7 +767,7 @@ I.sendDeviceKeyEvent(3);
 
 Set immediate value in app.
 
-See corresponding [webdriverio reference][12].
+See corresponding [webdriverio reference][14].
 
 Appium: support only iOS
 
@@ -776,7 +792,7 @@ I.setNetworkConnection(4) // airplane mode off, wifi off, data on
 I.setNetworkConnection(6) // airplane mode off, wifi on, data on
 ```
 
-See corresponding [webdriverio reference][13].
+See corresponding [webdriverio reference][15].
 
 Appium: support only Android
 
@@ -860,7 +876,7 @@ let locator = "#io.selendroid.testapp:id/LinearLayout1";
 I.swipe(locator, 800, 1200, 1000);
 ```
 
-[See complete reference][14]
+[See complete reference][16]
 
 #### Parameters
 
@@ -1118,10 +1134,14 @@ I.waitForVisible('#popup');
 
 [10]: http://webdriver.io/api/mobile/rotate.html
 
-[11]: https://developer.android.com/reference/android/view/KeyEvent.html
+[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
+
+[12]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
+
+[13]: https://developer.android.com/reference/android/view/KeyEvent.html
 
-[12]: http://webdriver.io/api/mobile/setImmediateValue.html
+[14]: http://webdriver.io/api/mobile/setImmediateValue.html
 
-[13]: http://webdriver.io/api/mobile/setNetworkConnection.html
+[15]: http://webdriver.io/api/mobile/setNetworkConnection.html
 
-[14]: http://webdriver.io/api/mobile/swipe.html
+[16]: http://webdriver.io/api/mobile/swipe.html
diff --git a/docs/helpers/WebDriver.md b/docs/helpers/WebDriver.md
index 1956c7c2..b3f54400 100644
--- a/docs/helpers/WebDriver.md
+++ b/docs/helpers/WebDriver.md
@@ -1279,6 +1279,22 @@ I.saveScreenshot('debug.png', true) //resizes to available scrollHeight and scro
 -   `fileName` **[string][18]** file name to save.
 -   `fullPage` **[boolean][31]** (optional, `false` by default) flag to enable fullscreen screenshot mode. 
 
+### scrollIntoView
+
+Scroll element into viewport.
+
+```js
+I.scrollIntoView('#submit');
+I.scrollIntoView('#submit', true);
+I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" });
+```
+
+#### Parameters
+
+-   `locator` **([string][18] | [object][19])** located by CSS|XPath|strict locator.
+-   `scrollIntoViewOptions`  
+-   `alignToTop` **([boolean][31] | [object][19])** (optional) or scrollIntoViewOptions (optional), see [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][32].
+
 ### scrollPageToBottom
 
 Scroll page to the bottom.
@@ -1589,7 +1605,7 @@ I.setCookie({name: 'auth', value: true});
 #### Parameters
 
 -   `cookie` **[object][19]** a cookie object.Uses Selenium's JSON [cookie
-    format][32].
+    format][33].
 
 ### setGeoLocation
 
@@ -1964,4 +1980,6 @@ I.waitUrlEquals('http://127.0.0.1:8000/info');
 
 [31]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
 
-[32]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
+[32]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
+
+[33]: https://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object
diff --git a/docs/hooks.md b/docs/hooks.md
index 0b50e296..6705037c 100644
--- a/docs/hooks.md
+++ b/docs/hooks.md
@@ -400,7 +400,9 @@ Test events provide a test object with following fields:
 * `tags` array of tags for this test
 * `file` path to a file with a test.
 * `steps` array of executed steps (available only in `test.passed`, `test.failed`, `test.finished` event)
-
+* `skipInfo` additional test options when test skipped 
+* * `message` string with reason for skip
+* * `description` string with test body
 and others
 
 #### Step Object
diff --git a/docs/webapi/scrollIntoView.mustache b/docs/webapi/scrollIntoView.mustache
new file mode 100644
index 00000000..930bf77d
--- /dev/null
+++ b/docs/webapi/scrollIntoView.mustache
@@ -0,0 +1,10 @@
+Scroll element into viewport.
+
+```js
+I.scrollIntoView('#submit');
+I.scrollIntoView('#submit', true);
+I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" });
+```
+
+@param {string|object} locator located by CSS|XPath|strict locator.
+@param {boolean|object} alignToTop (optional) or scrollIntoViewOptions (optional), see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView.
diff --git a/lib/codecept_rerunner.js b/lib/codecept_rerunner.js
new file mode 100644
index 00000000..a6ca3c5d
--- /dev/null
+++ b/lib/codecept_rerunner.js
@@ -0,0 +1,80 @@
+const fsPath = require('path');
+const container = require('./container');
+const event = require('./event');
+const BaseCodecept = require('./codecept');
+
+class CodeceptRerunner extends BaseCodecept {
+  runOnce(test) {
+    return new Promise((resolve, reject) => {
+      // @ts-ignore
+      container.createMocha();
+      const mocha = container.mocha();
+      this.testFiles.forEach((file) => {
+        delete require.cache[file];
+      });
+      mocha.files = this.testFiles;
+      if (test) {
+        if (!fsPath.isAbsolute(test)) {
+          test = fsPath.join(global.codecept_dir, test);
+        }
+        mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test);
+      }
+      try {
+        mocha.run((failures) => {
+          if (failures === 0) {
+            resolve();
+          } else {
+            reject(new Error(`${failures} tests fail`));
+          }
+        });
+      } catch (e) {
+        reject(e);
+      }
+    });
+  }
+
+  async runTests(test) {
+    const configRerun = this.config.rerun || {};
+    const minSuccess = configRerun.minSuccess || 1;
+    const maxReruns = configRerun.maxReruns || 1;
+    if (minSuccess > maxReruns) throw new Error('minSuccess must be less then maxReruns');
+    if (maxReruns === 1) {
+      await this.runOnce(test);
+      return;
+    }
+    let successCounter = 0;
+    let rerunsCounter = 0;
+    while (rerunsCounter < maxReruns && successCounter < minSuccess) {
+      rerunsCounter++;
+      try {
+        await this.runOnce(test);
+        successCounter++;
+        console.log(`\nProcess run ${rerunsCounter} of max ${maxReruns}, success runs ${successCounter}/${minSuccess} \n`);
+      } catch (e) {
+        console.error(e);
+        console.log(`\nFail run ${rerunsCounter} of max ${maxReruns}, success runs ${successCounter}/${minSuccess} \n`);
+      }
+    }
+    if (successCounter < minSuccess) {
+      throw new Error('Test suite unstable!');
+    }
+  }
+
+  run(test) {
+    const done = () => {
+      event.emit(event.all.result, this);
+      event.emit(event.all.after, this);
+    };
+    event.emit(event.all.before, this);
+    return this.runTests(test)
+      .then(() => {
+        this.teardown(done);
+      })
+      .catch((e) => {
+        this.teardown(done);
+        throw e;
+      });
+  }
+}
+
+module.exports = CodeceptRerunner;
diff --git a/lib/command/run-rerun.js b/lib/command/run-rerun.js
new file mode 100644
index 00000000..af0ebaed
--- /dev/null
+++ b/lib/command/run-rerun.js
@@ -0,0 +1,39 @@
+const getConfig = require('./utils').getConfig;
+const getTestRoot = require('./utils').getTestRoot;
+const printError = require('./utils').printError;
+const createOutputDir = require('./utils').createOutputDir;
+const Config = require('../config');
+const Codecept = require('../codecept_rerunner');
+
+module.exports = function (test, options) {
+  // registering options globally to use in config
+  process.profile = options.profile;
+  const configFile = options.config;
+  let codecept;
+
+  let config = getConfig(configFile);
+  if (options.override) {
+    config = Config.append(JSON.parse(options.override));
+  }
+  const testRoot = getTestRoot(configFile);
+  createOutputDir(config, testRoot);
+
+  function processError(err) {
+    printError(err);
+    process.exit(1);
+  }
+
+  try {
+    codecept = new Codecept(config, options);
+    codecept.init(testRoot);
+
+    codecept.runBootstrap((err) => {
+      if (err) throw new Error(`Error while running bootstrap file :${err}`);
+
+      codecept.loadTests();
+      codecept.run(test).catch(processError);
+    });
+  } catch (err) {
+    processError(err);
+  }
+};
diff --git a/lib/command/run.js b/lib/command/run.js
index 0fa81c04..f9a65ee2 100644
--- a/lib/command/run.js
+++ b/lib/command/run.js
@@ -3,31 +3,23 @@ const path = require('path');
 
 const getConfig = require('./utils').getConfig;
 const getTestRoot = require('./utils').getTestRoot;
-const fileExists = require('../utils').fileExists;
+const printError = require('./utils').printError;
+const createOutputDir = require('./utils').createOutputDir;
 const Config = require('../config');
 const Codecept = require('../codecept');
-const output = require('../output');
 
 module.exports = function (test, options) {
   // registering options globally to use in config
   process.profile = options.profile;
   const configFile = options.config;
   let codecept;
-  let outputDir;
 
-  const testRoot = getTestRoot(configFile);
   let config = getConfig(configFile);
   if (options.override) {
     config = Config.append(JSON.parse(options.override));
   }
-
-  if (path.isAbsolute(config.output)) outputDir = config.output;
-  else outputDir = path.join(testRoot, config.output);
-
-  if (!fileExists(outputDir)) {
-    output.print(`creating output directory: ${outputDir}`);
-    mkdirp.sync(outputDir);
-  }
+  const testRoot = getTestRoot(configFile);
+  createOutputDir(config, testRoot);
 
   try {
     codecept = new Codecept(config, options);
@@ -40,10 +32,7 @@ module.exports = function (test, options) {
       codecept.run(test);
     });
   } catch (err) {
-    output.print('');
-    output.error(err.message);
-    output.print('');
-    output.print(output.colors.grey(err.stack.replace(err.message, '')));
+    printError(err);
     process.exit(1);
   }
 };
diff --git a/lib/command/utils.js b/lib/command/utils.js
index 073e47cb..fafbccd9 100644
--- a/lib/command/utils.js
+++ b/lib/command/utils.js
@@ -2,10 +2,12 @@ const fs = require('fs');
 const path = require('path');
 const semver = require('semver');
 const util = require('util');
+const mkdirp = require('mkdirp');
 
 const output = require('../output');
 const { fileExists, beautify } = require('../utils');
 
+
 // alias to deep merge
 module.exports.deepMerge = require('../utils').deepMerge;
 
@@ -85,3 +87,21 @@ module.exports.captureStream = (stream) => {
     getData: () => buffer,
   };
 };
+
+module.exports.printError = (err) => {
+  output.print('');
+  output.error(err.message);
+  output.print('');
+  output.print(output.colors.grey(err.stack.replace(err.message, '')));
+};
+
+module.exports.createOutputDir = (config, testRoot) => {
+  let outputDir;
+  if (path.isAbsolute(config.output)) outputDir = config.output;
+  else outputDir = path.join(testRoot, config.output);
+
+  if (!fileExists(outputDir)) {
+    output.print(`creating output directory: ${outputDir}`);
+    mkdirp.sync(outputDir);
+  }
+};
diff --git a/lib/container.js b/lib/container.js
index 2f28506d..a27e6b36 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -32,7 +32,10 @@ class Container {
     if (config.grep && !opts.grep) {
       mochaConfig.grep = config.grep;
     }
-    container.mocha = MochaFactory.create(mochaConfig, opts || {});
+    this.createMocha = () => {
+      container.mocha = MochaFactory.create(mochaConfig, opts || {});
+    };
+    this.createMocha();
     container.helpers = createHelpers(config.helpers || {});
     container.translation = loadTranslation(config.translation || null);
     container.support = createSupportObjects(config.include || {});
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 96558b8f..70859261 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -1356,6 +1356,15 @@ class Appium extends Webdriver {
     return super.grabValueFrom(parseLocator.call(this, locator));
   }
 
+  /**
+   * {{> scrollIntoView }}
+   *
+   * Supported only for web testing
+   */
+  async scrollIntoView(locator, scrollIntoViewOptions) {
+    if (this.isWeb) return super.scrollIntoView(locator, scrollIntoViewOptions);
+  }
+
   /**
    * {{> seeCheckboxIsChecked }}
    *
@@ -1395,7 +1404,7 @@ class Appium extends Webdriver {
   /**
    * {{> selectOption }}
    *
-   * * Supported on only for web testing!
+   * Supported only for web testing
    */
   async selectOption(select, option) {
     if (this.isWeb) return super.selectOption(select, option);
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 4e042f75..cf2b7ef3 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -1315,6 +1315,17 @@ class WebDriver extends Helper {
     return this.browser.executeAsync.apply(this.browser, arguments);
   }
 
+  /**
+   * {{> scrollIntoView }}
+   *
+   */
+  async scrollIntoView(locator, scrollIntoViewOptions) {
+    const res = await this._locate(withStrictLocator(locator), true);
+    assertElementExists(res);
+    const elem = usingFirstElement(res);
+    return elem.scrollIntoView(scrollIntoViewOptions);
+  }
+
   /**
    * {{> scrollTo }}
    *
diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js
index 0f3c406d..b036e80d 100644
--- a/lib/plugin/allure.js
+++ b/lib/plugin/allure.js
@@ -74,6 +74,8 @@ const defaultConfig = {
 module.exports = (config) => {
   config = Object.assign(defaultConfig, config);
 
+  const plugin = {};
+
   const reporter = new Allure();
   reporter.setOptions({ targetDir: config.outputDir });
 
@@ -81,15 +83,31 @@ module.exports = (config) => {
   let currentStep;
   let isHookSteps = false;
 
-  this.addAttachment = (name, buffer, type) => {
+  reporter.pendingCase = function (testName, timestamp, opts = {}) {
+    reporter.startCase(testName, timestamp);
+
+    if (opts.description) plugin.setDescription(opts.description);
+    if (opts.severity) plugin.severity(opts.severity);
+    if (opts.severity) plugin.addLabel('tag', opts.severity);
+
+    reporter.endCase('pending', { message: opts.message || 'Test ignored' }, timestamp);
+  };
+
+  plugin.addAttachment = (name, buffer, type) => {
     reporter.addAttachment(name, buffer, type);
   };
 
-  this.setDescription = (description, type) => {
-    reporter.setDescription(description, type);
+  plugin.setDescription = (description, type) => {
+    const currentTest = reporter.getCurrentTest();
+    if (currentTest) {
+      currentTest.setDescription(description, type);
+    } else {
+      logger.error(`The test is not run. Please use "setDescription" for events:
+      "test.start", "test.before", "test.after", "test.passed", "test.failed", "test.finish"`);
+    }
   };
 
-  this.createStep = (name, stepFunc = () => {}) => {
+  plugin.createStep = (name, stepFunc = () => {}) => {
     let result;
     let status = 'passed';
     reporter.startStep(name);
@@ -111,7 +129,7 @@ module.exports = (config) => {
     return result;
   };
 
-  this.createAttachment = (name, content, type) => {
+  plugin.createAttachment = (name, content, type) => {
     if (typeof content === 'function') {
       const attachmentName = name;
       const buffer = content.apply(this, arguments);
@@ -119,27 +137,27 @@ module.exports = (config) => {
     } reporter.addAttachment(name, content, type);
   };
 
-  this.severity = (severity) => {
-    this.addLabel('severity', severity);
+  plugin.severity = (severity) => {
+    plugin.addLabel('severity', severity);
   };
 
-  this.epic = (epic) => {
-    this.addLabel('epic', epic);
+  plugin.epic = (epic) => {
+    plugin.addLabel('epic', epic);
   };
 
-  this.feature = (feature) => {
-    this.addLabel('feature', feature);
+  plugin.feature = (feature) => {
+    plugin.addLabel('feature', feature);
   };
 
-  this.story = (story) => {
-    this.addLabel('story', story);
+  plugin.story = (story) => {
+    plugin.addLabel('story', story);
   };
 
-  this.issue = (issue) => {
-    this.addLabel('issue', issue);
+  plugin.issue = (issue) => {
+    plugin.addLabel('issue', issue);
   };
 
-  this.addLabel = (name, value) => {
+  plugin.addLabel = (name, value) => {
     const currentTest = reporter.getCurrentTest();
     if (currentTest) {
       currentTest.addLabel(name, value);
@@ -156,16 +174,16 @@ module.exports = (config) => {
   event.dispatcher.on(event.suite.before, (suite) => {
     for (const test of suite.tests) {
       if (test.pending) {
-        reporter.pendingCase(test.title);
+        reporter.pendingCase(test.title, null, test.opts.skipInfo);
       }
     }
   });
 
-  event.dispatcher.on(event.hook.started, (hook) => {
+  event.dispatcher.on(event.hook.started, () => {
     isHookSteps = true;
   });
 
-  event.dispatcher.on(event.hook.passed, (hook) => {
+  event.dispatcher.on(event.hook.passed, () => {
     isHookSteps = false;
   });
 
@@ -200,7 +218,7 @@ module.exports = (config) => {
     reporter.endCase('failed', err);
   });
 
-  event.dispatcher.on(event.test.passed, (test) => {
+  event.dispatcher.on(event.test.passed, () => {
     if (currentStep) reporter.endStep('passed');
     if (currentMetaStep.length) {
       currentMetaStep.forEach(() => reporter.endStep('passed'));
@@ -263,5 +281,5 @@ module.exports = (config) => {
     }
   }
 
-  return this;
+  return plugin;
 };
diff --git a/lib/ui.js b/lib/ui.js
index 263d7d3d..141c863f 100644
--- a/lib/ui.js
+++ b/lib/ui.js
@@ -144,6 +144,26 @@ module.exports = function (suite) {
       return context.Scenario(title, {});
     };
 
+    /**
+     * Pending test case with message: 'Test not implemented!'.
+     * @global
+     * @kind constant
+     * @type {CodeceptJS.IScenario}
+     */
+    context.Scenario.todo = function (title, opts = {}, fn) {
+      if (typeof opts === 'function' && !fn) {
+        fn = opts;
+        opts = {};
+      }
+
+      const skipInfo = {
+        message: 'Test not implemented!',
+        description: fn ? fn.toString() : '',
+      };
+
+      return context.Scenario(title, { ...opts, skipInfo });
+    };
+
     addDataContext(context);
   });
 
diff --git a/package.json b/package.json
index 20ee48be..001b08c1 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,9 @@
     "lint": "eslint bin/ examples/ lib/ test/ translations/ runio.js",
     "lint-fix": "eslint bin/ examples/ lib/ test/ translations/ runio.js --fix",
     "docs": "./runio.js docs",
-    "test": "mocha test/unit --recursive && mocha test/runner --recursive",
+    "test:unit": "mocha test/unit --recursive",
+    "test:runner": "mocha test/runner --recursive",
+    "test": "npm run test:unit && npm run test:runner",
     "def": "./runio.js def",
     "dev:graphql": "nodemon test/data/graphql/index.js",
     "publish:site": "./runio.js publish:site"
diff --git a/typings/types.d.ts b/typings/types.d.ts
index 5770b3e7..35fecb9c 100644
--- a/typings/types.d.ts
+++ b/typings/types.d.ts
@@ -814,6 +814,22 @@ declare namespace CodeceptJS {
          *
          */
         grabValueFrom(locator: CodeceptJS.LocatorOrString): Promise<string>;
+        /**
+         * Scroll element into viewport.
+         *
+         * ```js
+         * I.scrollIntoView('#submit');
+         * I.scrollIntoView('#submit', true);
+         * I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" });
+         * ```
+         *
+         * @param {string|object} locator located by CSS|XPath|strict locator.
+         * @param {boolean|object} alignToTop (optional) or scrollIntoViewOptions (optional), see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView.
+         *
+         *
+         * Supported only for web testing
+         */
+        scrollIntoView(locator: string | any, alignToTop: boolean | any): void;
         /**
          * Verifies that the specified checkbox is checked.
          *
@@ -891,7 +907,7 @@ declare namespace CodeceptJS {
          * @param {CodeceptJS.LocatorOrString} select field located by label|name|CSS|XPath|strict locator.
          * @param {string|Array<*>} option visible text or value of option.
          *
-         * * Supported on only for web testing!
+         * Supported only for web testing
          */
         selectOption(select: CodeceptJS.LocatorOrString, option: string | any[]): void;
         /**
@@ -6322,6 +6338,21 @@ declare namespace CodeceptJS {
          *
          */
         executeAsyncScript(fn: string | ((...params: any[]) => any), ...args: any[]): Promise<any>;
+        /**
+         * Scroll element into viewport.
+         *
+         * ```js
+         * I.scrollIntoView('#submit');
+         * I.scrollIntoView('#submit', true);
+         * I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" });
+         * ```
+         *
+         * @param {string|object} locator located by CSS|XPath|strict locator.
+         * @param {boolean|object} alignToTop (optional) or scrollIntoViewOptions (optional), see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView.
+         *
+         *
+         */
+        scrollIntoView(locator: string | any, alignToTop: boolean | any): void;
         /**
          * Scrolls to element matched by locator.
          * Extra shift can be set with offsetX and offsetY options.
@@ -7696,10 +7727,21 @@ declare namespace CodeceptJS {
          */
         scrollTo(locator: CodeceptJS.LocatorOrString, offsetX?: number, offsetY?: number): void;
         /**
-         * {{> moveCursorTo}}
+         * Moves cursor to element matched by locator.
+         * Extra shift can be set with offsetX and offsetY options.
+         *
+         * ```js
+         * I.moveCursorTo('.tooltip');
+         * I.moveCursorTo('#submit', 5,5);
+         * ```
+         *
+         * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
+         * @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
+         * @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
+         *
          * Appium: support only web testing
          */
-        moveCursorTo(): void;
+        moveCursorTo(locator: CodeceptJS.LocatorOrString, offsetX?: number, offsetY?: number): void;
         /**
          * {{> saveScreenshot}}
          * Appium: support
@@ -7714,25 +7756,56 @@ declare namespace CodeceptJS {
          */
         setCookie(): void;
         /**
-         * {{> clearCookie}}
+         * Clears a cookie by name,
+         * if none provided clears all cookies.
+         *
+         * ```js
+         * I.clearCookie();
+         * I.clearCookie('test');
+         * ```
+         *
+         * @param {?string} [cookie=null] (optional, `null` by default) cookie name
          * Appium: support only web testing
          */
-        clearCookie(): void;
+        clearCookie(cookie?: string): void;
         /**
-         * {{> seeCookie}}
+         * Checks that cookie with given name exists.
+         *
+         * ```js
+         * I.seeCookie('Auth');
+         * ```
+         *
+         * @param {string} name cookie name.
+         *
          * Appium: support only web testing
          */
-        seeCookie(): void;
+        seeCookie(name: string): void;
         /**
-         * {{> dontSeeCookie}}
+         * Checks that cookie with given name does not exist.
+         *
+         * ```js
+         * I.dontSeeCookie('auth'); // no auth cookie
+         * ```
+         *
+         * @param {string} name cookie name.
          * Appium: support only web testing
          */
-        dontSeeCookie(): void;
+        dontSeeCookie(name: string): void;
         /**
-         * {{> grabCookie}}
+         * Gets a cookie object by name.
+         * If none provided gets all cookies.
+         * Resumes test execution, so **should be used inside async with `await`** operator.
+         *
+         * ```js
+         * let cookie = await I.grabCookie('auth');
+         * assert(cookie.value, '123456');
+         * ```
+         *
+         * @param {?string} [name=null] cookie name.
+         * @returns {Promise<string>} attribute value
          * Appium: support only web testing
          */
-        grabCookie(): void;
+        grabCookie(name?: string): Promise<string>;
         /**
          * Accepts the active JavaScript native popup window, as created by window.alert|window.confirm|window.prompt.
          * Don't confuse popups with modal windows, as created by [various
@@ -8143,18 +8216,20 @@ declare namespace CodeceptJS {
          */
         runInWeb(): void;
     }
-    /**
-     * @interface
-     * @alias ActorStatic
-     */
-    interface ActorStatic {
+    namespace ActorStatic {
         /**
          * add print comment method`
          * @param {string} msg
          * @param {string} color
          * @return {Promise<any> | undefined}
          */
-        say(msg: string, color: string): Promise<any> | undefined;
+        function say(msg: string, color: string): Promise<any> | undefined;
+    }
+    /**
+     * @interface
+     * @alias ActorStatic
+     */
+    interface ActorStatic {
         /**
          * @function
          * @param {*} opts
@@ -8363,6 +8438,16 @@ declare namespace CodeceptJS {
          */
         filter(func: (...params: any[]) => any): void;
     }
+    namespace event {
+        /**
+         * @param {string} event
+         * @param {*} param
+         */
+        function emit(event: string, param: any): void;
+        /** for testing only!
+         */
+        function cleanDispatcher(): void;
+    }
     /**
      * @interface
      * @alias event
@@ -8451,14 +8536,6 @@ declare namespace CodeceptJS {
             before: 'multiple.before';
             after: 'multiple.after';
         };
-        /**
-         * @param {string} event
-         * @param {*} param
-         */
-        emit(event: string, param: any): void;
-        /** for testing only!
-         */
-        cleanDispatcher(): void;
     }
     class Helper {
         /**
@@ -8571,89 +8648,91 @@ declare namespace CodeceptJS {
          */
         debugSection(section: string, msg: string): void;
     }
-    /**
-     * @alias CodeceptJS.browserCodecept
-     * @interface
-     */
-    interface browserCodecept {
-        /**
-         * all found elements are stored here for reuse
-         * @inner
-         * @type {Node[]}
-         */
-        elements: Node[];
-        /**
-         * global context changer
-         * @inner
-         * @type {?Node}
-         */
-        within: Node;
+    namespace browserCodecept {
         /**
          * finders
          * @param {number} id
          * @return {Node}
          */
-        fetchElement(id: number): Node;
+        function fetchElement(id: number): Node;
         /**
          * @param   {string}  by
          * @param   {CodeceptJS.ILocator}  locator
          * @param   {*}  [contextEl]
          * @return  {number[]}
          */
-        findAndStoreElements(by: string, locator: CodeceptJS.ILocator, contextEl?: any): number[];
+        function findAndStoreElements(by: string, locator: CodeceptJS.ILocator, contextEl?: any): number[];
         /**
          * @param   {string}  by
          * @param   {CodeceptJS.ILocator}  locator
          * @param   {*}  [contextEl]
          * @return  {number | undefined}
          */
-        findAndStoreElement(by: string, locator: CodeceptJS.ILocator, contextEl?: any): number | undefined;
+        function findAndStoreElement(by: string, locator: CodeceptJS.ILocator, contextEl?: any): number | undefined;
         /**
          * @param {string} by
          * @param {CodeceptJS.ILocator} locator
          */
-        setWithin(by: string, locator: CodeceptJS.ILocator): void;
+        function setWithin(by: string, locator: CodeceptJS.ILocator): void;
         /**
          * @param   {string}  by
          * @param   {CodeceptJS.ILocator}  locator
          * @param   {*}  [contextEl]
          * @return  {Node[]}
          */
-        findElements(by: string, locator: CodeceptJS.ILocator, contextEl?: any): Node[];
+        function findElements(by: string, locator: CodeceptJS.ILocator, contextEl?: any): Node[];
         /**
          * @param   {string}  by
          * @param   {CodeceptJS.ILocator}  locator
          * @param   {*}  [context]
          * @return  {?Node}
          */
-        findElement(by: string, locator: CodeceptJS.ILocator, context?: any): Node;
+        function findElement(by: string, locator: CodeceptJS.ILocator, context?: any): Node;
         /**
          * @param {number} el
          * @return {boolean}
          */
-        clickEl(el: number): boolean;
+        function clickEl(el: number): boolean;
         /** @param {number} el
          */
-        doubleClickEl(el: number): void;
+        function doubleClickEl(el: number): void;
         /**
          * @param {number} el
          * @param {number | undefined} x
          * @param {number | undefined} y
          */
-        hoverEl(el: number, x: number | undefined, y: number | undefined): void;
+        function hoverEl(el: number, x: number | undefined, y: number | undefined): void;
         /** @param {number} el
          */
-        rightClickEl(el: number): void;
+        function rightClickEl(el: number): void;
         /**
          * @param {number} el
          * @return {boolean  |  undefined}
          */
-        checkEl(el: number): boolean | undefined;
+        function checkEl(el: number): boolean | undefined;
         /**
          * @param {number} el
          * @return {boolean}
          */
-        unCheckEl(el: number): boolean;
+        function unCheckEl(el: number): boolean;
+    }
+    /**
+     * @alias CodeceptJS.browserCodecept
+     * @interface
+     */
+    interface browserCodecept {
+        /**
+         * all found elements are stored here for reuse
+         * @inner
+         * @type {Node[]}
+         */
+        elements: Node[];
+        /**
+         * global context changer
+         * @inner
+         * @type {?Node}
+         */
+        within: Node;
     }
     /**
      * Index file for loading CodeceptJS programmatically.
@@ -8912,59 +8991,75 @@ declare namespace CodeceptJS {
          */
         static build(locator: CodeceptJS.LocatorOrString): Locator;
     }
-    /**
-     * @alias output
-     * @interface
-     */
-    interface output {
-        /**
-         * @type {number}
-         * @inner
-         */
-        stepShift: number;
+    namespace output {
         /**
          * Set or return current verbosity level
          * @param {number} level
          * @return {number}
          */
-        level(level: number): number;
+        function level(level: number): number;
         /**
          * Print information for a process
          * Used in multiple-run
          * @param {string} process
          * @return {string}
          */
-        process(process: string): string;
+        function process(process: string): string;
         /**
          * Print information in --debug mode
          * @param {string} msg
          */
-        debug(msg: string): void;
+        function debug(msg: string): void;
         /**
          * Print information in --verbose mode
          * @param {string} msg
          */
-        log(msg: string): void;
+        function log(msg: string): void;
         /**
          * Print error
          * @param {string} msg
          */
-        error(msg: string): void;
+        function error(msg: string): void;
         /**
          * Print a successful message
          * @param {string} msg
          */
-        success(msg: string): void;
+        function success(msg: string): void;
         /**
          * @param {string} name
          * @param {string} msg
          */
-        plugin(name: string, msg: string): void;
+        function plugin(name: string, msg: string): void;
         /**
          * Print a step
          * @param {CodeceptJS.Step} step
          */
-        step(step: CodeceptJS.Step): void;
+        function step(step: CodeceptJS.Step): void;
+        /**
+         *
+         * Print a text in console log
+         * @param {string} message
+         * @param {string} [color]
+         */
+        function say(message: string, color?: string): void;
+        /**
+         * @param {number} passed
+         * @param {number} failed
+         * @param {number} skipped
+         * @param {number} duration
+         */
+        function result(passed: number, failed: number, skipped: number, duration: number): void;
+    }
+    /**
+     * @alias output
+     * @interface
+     */
+    interface output {
+        /**
+         * @type {number}
+         * @inner
+         */
+        stepShift: number;
         /**
          * @name CodeceptJS.output~suite
          * @type {CodeceptJS.OutputSuite}
@@ -8983,120 +9078,94 @@ declare namespace CodeceptJS {
          * @inner
          */
         scenario: CodeceptJS.OutputScenario;
+    }
+    namespace OutputSuite {
         /**
-         *
-         * Print a text in console log
-         * @param {string} message
-         * @param {string} color
-         */
-        say(message: string, color: string): void;
-        /**
-         * @param {number} passed
-         * @param {number} failed
-         * @param {number} skipped
-         * @param {number} duration
+         * @param {Mocha.Suite} suite
          */
-        result(passed: number, failed: number, skipped: number, duration: number): void;
+        function started(suite: Mocha.Suite): void;
     }
     /**
      * @alias OutputSuite
      * @interface
      */
     interface OutputSuite {
-        /**
-         * @param {Mocha.Suite} suite
-         */
-        started(suite: Mocha.Suite): void;
     }
-    /**
-     * @alias OutputTest
-     * @interface
-     */
-    interface OutputTest {
+    namespace OutputTest {
         /**
          * @param {Mocha.Test} test
          */
-        started(test: Mocha.Test): void;
+        function started(test: Mocha.Test): void;
         /**
          * @param {Mocha.Test} test
          */
-        passed(test: Mocha.Test): void;
+        function passed(test: Mocha.Test): void;
         /**
          * @param {Mocha.Test} test
          */
-        failed(test: Mocha.Test): void;
+        function failed(test: Mocha.Test): void;
         /**
          * @param {Mocha.Test} test
          */
-        skipped(test: Mocha.Test): void;
+        function skipped(test: Mocha.Test): void;
     }
     /**
-     * @alias OutputScenario
+     * @alias OutputTest
      * @interface
      */
-    interface OutputScenario {
+    interface OutputTest {
+    }
+    namespace OutputScenario {
         /**
          * @param {Mocha.Test} test
          */
-        started(test: Mocha.Test): void;
+        function started(test: Mocha.Test): void;
         /**
          * @param {Mocha.Test} test
          */
-        passed(test: Mocha.Test): void;
+        function passed(test: Mocha.Test): void;
         /**
          * @param {Mocha.Test} test
          */
-        failed(test: Mocha.Test): void;
+        function failed(test: Mocha.Test): void;
     }
     /**
-     * Pauses test execution and starts interactive shell
+     * @alias OutputScenario
+     * @interface
      */
-    function pause(): void;
+    interface OutputScenario {
+    }
     /**
-     * Singleton object to record all test steps as promises and run them in chain.
-     * @alias recorder
-     * @interface
+     * Pauses test execution and starts interactive shell
      */
-    interface recorder {
-        /**
-         * @type {Array<Object<string, *>>}
-         * @inner
-         */
-        retries: {
-            [key: string]: any;
-        }[];
+    function pause(): void;
+    namespace recorder {
         /**
          * Start recording promises
          *
          * @api
          */
-        start(): void;
+        function start(): void;
         /** @return {boolean}
          */
-        isRunning(): boolean;
+        function isRunning(): boolean;
         /** @return {void}
          */
-        startUnlessRunning(): void;
+        function startUnlessRunning(): void;
         /**
          * Add error handler to catch rejected promises
          *
          * @api
          * @param {function} fn
          */
-        errHandler(fn: (...params: any[]) => any): void;
+        function errHandler(fn: (...params: any[]) => any): void;
         /**
          * Stops current promise chain, calls `catch`.
          * Resets recorder to initial state.
          *
          * @api
          */
-        reset(): void;
-        /**
-         * @name CodeceptJS.recorder~session
-         * @type {CodeceptJS.RecorderSession}
-         * @inner
-         */
-        session: CodeceptJS.RecorderSession;
+        function reset(): void;
         /**
          * Adds a promise to a chain.
          * Promise description should be passed as first parameter.
@@ -9109,80 +9178,102 @@ declare namespace CodeceptJS {
          *     false: ignore `retryOpts` and won't retry.
          * @return {Promise<*> | undefined}
          */
-        add(taskName: string, fn?: (...params: any[]) => any, force?: boolean, retry?: boolean): Promise<any> | undefined;
+        function add(taskName: string, fn?: (...params: any[]) => any, force?: boolean, retry?: boolean): Promise<any> | undefined;
         /**
          * @param {*} opts
          * @return {*}
          */
-        retry(opts: any): any;
+        function retry(opts: any): any;
         /**
          * @param {function} [customErrFn]
          * @return {Promise<*>}
          */
-        catch(customErrFn?: (...params: any[]) => any): Promise<any>;
+        function catch(customErrFn?: (...params: any[]) => any): Promise<any>;
         /**
          * @param {function} customErrFn
          * @return {Promise<*>}
          */
-        catchWithoutStop(customErrFn: (...params: any[]) => any): Promise<any>;
+        function catchWithoutStop(customErrFn: (...params: any[]) => any): Promise<any>;
         /**
          * Adds a promise which throws an error into a chain
          *
          * @api
          * @param {*} err
          */
-        throw(err: any): void;
+        function throw(err: any): void;
         /** @param {*} err
          */
-        saveFirstAsyncError(err: any): void;
+        function saveFirstAsyncError(err: any): void;
         /** @return {*}
          */
-        getAsyncErr(): any;
+        function getAsyncErr(): any;
         /** @return {void}
          */
-        cleanAsyncErr(): void;
+        function cleanAsyncErr(): void;
         /**
          * Stops recording promises
          * @api
          */
-        stop(): void;
+        function stop(): void;
         /**
          * Get latest promise in chain.
          *
          * @api
          * @return {Promise<*>}
          */
-        promise(): Promise<any>;
+        function promise(): Promise<any>;
         /**
          * Get a list of all chained tasks
          * @return {string}
          */
-        scheduled(): string;
+        function scheduled(): string;
         /**
          * Get a state of current queue and tasks
          * @return {string}
          */
-        toString(): string;
+        function toString(): string;
     }
     /**
-     * @alias RecorderSession
+     * Singleton object to record all test steps as promises and run them in chain.
+     * @alias recorder
      * @interface
      */
-    interface RecorderSession {
+    interface recorder {
         /**
-         * @type {boolean}
+         * @type {Array<Object<string, *>>}
          * @inner
          */
-        running: boolean;
+        retries: {
+            [key: string]: any;
+        }[];
+        /**
+         * @name CodeceptJS.recorder~session
+         * @type {CodeceptJS.RecorderSession}
+         * @inner
+         */
+        session: CodeceptJS.RecorderSession;
+    }
+    namespace RecorderSession {
         /** @param {string} name
          */
-        start(name: string): void;
+        function start(name: string): void;
         /** @param {string} name
          */
-        restore(name: string): void;
+        function restore(name: string): void;
         /** @param {function} fn
          */
-        catch(fn: (...params: any[]) => any): void;
+        function catch(fn: (...params: any[]) => any): void;
+    }
+    /**
+     * @alias RecorderSession
+     * @interface
+     */
+    interface RecorderSession {
+        /**
+         * @type {boolean}
+         * @inner
+         */
+        running: boolean;
     }
     class Secret {
         constructor(secret: string);
@@ -9426,10 +9517,10 @@ declare namespace CodeceptJS {
          * I.tap({ ios: 'Save', android: 'SAVE' }, '#main'); // different texts on iOS and Android
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
-         * @param {?CodeceptJS.LocatorOrString} [context=null]
+         * @param {string|object} locator
+         * @param {string|object} [context=null]
          */
-        tap(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString): void;
+        tap(locator: string | any, context?: string | any): void;
         /**
          * Multi taps on an element.
          * Element can be located by its text or id or accessibility id.
@@ -9444,11 +9535,11 @@ declare namespace CodeceptJS {
          * I.multiTap('Update', 2, '#menu'); // locate by id
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {number} num number of taps
-         * @param {?CodeceptJS.LocatorOrString} [context=null] context element
+         * @param {string|object} locator element to locate
+         * @param {int} num number of taps
+         * @param {string|object} [context=null] context element
          */
-        multiTap(locator: CodeceptJS.LocatorOrString, num: number, context?: CodeceptJS.LocatorOrString): void;
+        multiTap(locator: string | any, num: int, context?: string | any): void;
         /**
          * Taps an element and holds for a requested time.
          *
@@ -9458,11 +9549,11 @@ declare namespace CodeceptJS {
          * I.longPress('Update', 2, '#menu'); // locate by text inside #menu, hold for 2 seconds
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {number} sec number of seconds to hold tap
-         * @param {CodeceptJS.LocatorOrString} context context element
+         * @param {string|object} locator element to locate
+         * @param {num} sec number of seconds to hold tap
+         * @param {string|object} context context element
          */
-        longPress(locator: CodeceptJS.LocatorOrString, sec: number, context: CodeceptJS.LocatorOrString): void;
+        longPress(locator: string | any, sec: num, context: string | any): void;
         /**
          * Clicks on an element.
          * Element can be located by its text or id or accessibility id
@@ -9479,10 +9570,10 @@ declare namespace CodeceptJS {
          * I.click({ ios: 'Save', android: 'SAVE' }, '#main'); // different texts on iOS and Android
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
-         * @param {?CodeceptJS.LocatorOrString} [context=null]
+         * @param {string|object} locator
+         * @param {string|object} [context=null]
          */
-        click(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString): void;
+        click(locator: string | any, context?: string | any): void;
         /**
          * Performs click on element with horizontal and vertical offset.
          * An element is located by text, id, accessibility id.
@@ -9492,12 +9583,12 @@ declare namespace CodeceptJS {
          * I.clickAtPoint('~save', 10, 10); // locate by accessibility id
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
-         * @param {number} [x=0] horizontal offset
-         * @param {number} [y=0] vertical offset
+         * @param {string|object} locator
+         * @param {int} [x=0] horizontal offset
+         * @param {int} [y=0] vertical offset
          *
          */
-        clickAtPoint(locator: CodeceptJS.LocatorOrString, x?: number, y?: number): void;
+        clickAtPoint(locator: string | any, x?: int, y?: int): void;
         /**
          * Checks text to be visible.
          * Use second parameter to narrow down the search.
@@ -9509,9 +9600,9 @@ declare namespace CodeceptJS {
          * ```
          *
          * @param {string} text to check visibility
-         * @param {?CodeceptJS.LocatorOrString} [context=null] element inside which to search for text
+         * @param {string|object} [context=null] element inside which to search for text
          */
-        see(text: string, context?: CodeceptJS.LocatorOrString): void;
+        see(text: string, context?: string | any): void;
         /**
          * Checks text not to be visible.
          * Use second parameter to narrow down the search.
@@ -9522,9 +9613,9 @@ declare namespace CodeceptJS {
          * I.dontSee('Record deleted', '~message');
          * ```
          * @param {string} text to check invisibility
-         * @param {CodeceptJS.LocatorOrString} context element in which to search for text
+         * @param {string|object} context element in which to search for text
          */
-        dontSee(text: string, context: CodeceptJS.LocatorOrString): void;
+        dontSee(text: string, context: string | any): void;
         /**
          * Checks for visibility of an element.
          * Use second parameter to narrow down the search.
@@ -9534,10 +9625,10 @@ declare namespace CodeceptJS {
          * I.seeElement('~edit', '#menu'); // element inside #menu
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {?CodeceptJS.LocatorOrString} [context=null] context element
+         * @param {string|object} locator element to locate
+         * @param {string|object} [context=null] context element
          */
-        seeElement(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString): void;
+        seeElement(locator: string | any, context?: string | any): void;
         /**
          * Checks that element is not visible.
          * Use second parameter to narrow down the search.
@@ -9546,10 +9637,10 @@ declare namespace CodeceptJS {
          * I.dontSeeElement('~edit'); // located by accessibility id
          * I.dontSeeElement('~edit', '#menu'); // element inside #menu
          * ```
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {?CodeceptJS.LocatorOrString} [context=null] context element
+         * @param {string|object} locator element to locate
+         * @param {string|object} [context=null] context element
          */
-        dontSeeElement(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString): void;
+        dontSeeElement(locator: string | any, context?: string | any): void;
         /**
          * Checks for existence of an element. An element can be visible or not.
          * Use second parameter to narrow down the search.
@@ -9559,10 +9650,10 @@ declare namespace CodeceptJS {
          * I.seeElementExists('~edit', '#menu'); // element inside #menu
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {CodeceptJS.LocatorOrString} context  context element
+         * @param {string|object} locator element to locate
+         * @param {string|object} context  context element
          */
-        seeElementExists(locator: CodeceptJS.LocatorOrString, context: CodeceptJS.LocatorOrString): void;
+        seeElementExists(locator: string | any, context: string | any): void;
         /**
          * Checks that element not exists.
          * Use second parameter to narrow down the search.
@@ -9572,10 +9663,10 @@ declare namespace CodeceptJS {
          * I.dontSeeElementExist('~edit', '#menu'); // element inside #menu
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator element to locate
-         * @param {CodeceptJS.LocatorOrString} context context element
+         * @param {string|object} locator element to locate
+         * @param {string|object} context context element
          */
-        dontSeeElementExists(locator: CodeceptJS.LocatorOrString, context: CodeceptJS.LocatorOrString): void;
+        dontSeeElementExists(locator: string | any, context: string | any): void;
         /**
          * Fills in text field in an app.
          * A field can be located by text, accessibility id, id.
@@ -9586,10 +9677,10 @@ declare namespace CodeceptJS {
          * I.fillField({ android: 'NAME', ios: 'name' }, 'davert');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} field an input element to fill in
+         * @param {string|object} field an input element to fill in
          * @param {string} value value to fill
          */
-        fillField(field: CodeceptJS.LocatorOrString, value: string): void;
+        fillField(field: string | any, value: string): void;
         /**
          * Clears a text field.
          * A field can be located by text, accessibility id, id.
@@ -9598,9 +9689,9 @@ declare namespace CodeceptJS {
          * I.clearField('~name');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} field an input element to clear
+         * @param {string|object} field an input element to clear
          */
-        clearField(field: CodeceptJS.LocatorOrString): void;
+        clearField(field: string | any): void;
         /**
          * Appends text into the field.
          * A field can be located by text, accessibility id, id.
@@ -9609,10 +9700,10 @@ declare namespace CodeceptJS {
          * I.appendField('name', 'davert');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} field
+         * @param {string|object} field
          * @param {string} value
          */
-        appendField(field: CodeceptJS.LocatorOrString, value: string): void;
+        appendField(field: string | any, value: string): void;
         /**
          * Scrolls to the top of an element.
          *
@@ -9620,9 +9711,9 @@ declare namespace CodeceptJS {
          * I.scrollUp('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
+         * @param {string|object} locator
          */
-        scrollUp(locator: CodeceptJS.LocatorOrString): void;
+        scrollUp(locator: string | any): void;
         /**
          * Scrolls to the bottom of an element.
          *
@@ -9630,9 +9721,9 @@ declare namespace CodeceptJS {
          * I.scrollDown('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
+         * @param {string|object} locator
          */
-        scrollDown(locator: CodeceptJS.LocatorOrString): void;
+        scrollDown(locator: string | any): void;
         /**
          * Scrolls to the left of an element.
          *
@@ -9640,9 +9731,9 @@ declare namespace CodeceptJS {
          * I.scrollLeft('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
+         * @param {string|object} locator
          */
-        scrollLeft(locator: CodeceptJS.LocatorOrString): void;
+        scrollLeft(locator: string | any): void;
         /**
          * Scrolls to the right of an element.
          *
@@ -9650,9 +9741,9 @@ declare namespace CodeceptJS {
          * I.scrollRight('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator
+         * @param {string|object} locator
          */
-        scrollRight(locator: CodeceptJS.LocatorOrString): void;
+        scrollRight(locator: string | any): void;
         /**
          * Performs a swipe up inside an element.
          * Can be `slow` or `fast` swipe.
@@ -9661,10 +9752,10 @@ declare namespace CodeceptJS {
          * I.swipeUp('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element on which to perform swipe
+         * @param {string|object} locator an element on which to perform swipe
          * @param {string} [speed='slow'] a speed to perform: `slow` or `fast`.
          */
-        swipeUp(locator: CodeceptJS.LocatorOrString, speed?: string): void;
+        swipeUp(locator: string | any, speed?: string): void;
         /**
          * Performs a swipe up inside an element.
          * Can be `slow` or `fast` swipe.
@@ -9673,10 +9764,10 @@ declare namespace CodeceptJS {
          * I.swipeUp('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element on which to perform swipe
+         * @param {string|object} locator an element on which to perform swipe
          * @param {string} [speed='slow'] a speed to perform: `slow` or `fast`.
          */
-        swipeDown(locator: CodeceptJS.LocatorOrString, speed?: string): void;
+        swipeDown(locator: string | any, speed?: string): void;
         /**
          * Performs a swipe up inside an element.
          * Can be `slow` or `fast` swipe.
@@ -9685,10 +9776,10 @@ declare namespace CodeceptJS {
          * I.swipeUp('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element on which to perform swipe
+         * @param {string|object} locator an element on which to perform swipe
          * @param {string} [speed='slow'] a speed to perform: `slow` or `fast`.
          */
-        swipeLeft(locator: CodeceptJS.LocatorOrString, speed?: string): void;
+        swipeLeft(locator: string | any, speed?: string): void;
         /**
          * Performs a swipe up inside an element.
          * Can be `slow` or `fast` swipe.
@@ -9697,10 +9788,10 @@ declare namespace CodeceptJS {
          * I.swipeUp('#container');
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element on which to perform swipe
+         * @param {string|object} locator an element on which to perform swipe
          * @param {string} [speed='slow'] a speed to perform: `slow` or `fast`.
          */
-        swipeRight(locator: CodeceptJS.LocatorOrString, speed?: string): void;
+        swipeRight(locator: string | any, speed?: string): void;
         /**
          * Waits for number of seconds
          *
@@ -9708,9 +9799,9 @@ declare namespace CodeceptJS {
          * I.wait(2); // waits for 2 seconds
          * ```
          *
-         * @param {number} sec number of seconds to wait
+         * @param {int} sec number of seconds to wait
          */
-        wait(sec: number): void;
+        wait(sec: int): void;
         /**
          * Waits for an element to exist on page.
          *
@@ -9718,10 +9809,10 @@ declare namespace CodeceptJS {
          * I.waitForElement('#message', 1); // wait for 1 second
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element to wait for
-         * @param {number} [sec=5] number of seconds to wait, 5 by default
+         * @param {string|object} locator an element to wait for
+         * @param {int} [sec=5] number of seconds to wait, 5 by default
          */
-        waitForElement(locator: CodeceptJS.LocatorOrString, sec?: number): void;
+        waitForElement(locator: string | any, sec?: int): void;
         /**
          * Waits for an element to be visible on page.
          *
@@ -9729,10 +9820,10 @@ declare namespace CodeceptJS {
          * I.waitForElementVisible('#message', 1); // wait for 1 second
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator an element to wait for
-         * @param {number} [sec=5] number of seconds to wait
+         * @param {string|object} locator an element to wait for
+         * @param {int} [sec=5] number of seconds to wait
          */
-        waitForElementVisible(locator: CodeceptJS.LocatorOrString, sec?: number): void;
+        waitForElementVisible(locator: string | any, sec?: int): void;
         /**
          * Waits an elment to become not visible.
          *
@@ -9740,10 +9831,10 @@ declare namespace CodeceptJS {
          * I.waitToHide('#message', 2); // wait for 2 seconds
          * ```
          *
-         * @param {CodeceptJS.LocatorOrString} locator  an element to wait for
-         * @param {number} [sec=5] number of seconds to wait
+         * @param {string|object} locator  an element to wait for
+         * @param {int} [sec=5] number of seconds to wait
          */
-        waitToHide(locator: CodeceptJS.LocatorOrString, sec?: number): void;
+        waitToHide(locator: string | any, sec?: int): void;
     }
 }
 


Test patch:
diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js
index f6f81b1d..ba66ebee 100644
--- a/lib/command/workers/runTests.js
+++ b/lib/command/workers/runTests.js
@@ -20,7 +20,7 @@ const stderr = '';
 
 
 // Requiring of Codecept need to be after tty.getWindowSize is available.
-const Codecept = require('../../codecept');
+const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept');
 
 const {
   options, tests, testRoot, workerIndex,
diff --git a/test/data/app/view/form/scroll_into_view.php b/test/data/app/view/form/scroll_into_view.php
new file mode 100644
index 00000000..d465c2fd
--- /dev/null
+++ b/test/data/app/view/form/scroll_into_view.php
@@ -0,0 +1,13 @@
+<html>
+  <head>
+    <style>
+      #notInViewportByDefault {
+        border: 1px solid black;
+        margin-top: 2000px;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="notInViewportByDefault">Div not in viewport by default</div>
+  </body>
+</html>
diff --git a/test/data/sandbox/configs/run-rerun/codecept.conf.fail_test.js b/test/data/sandbox/configs/run-rerun/codecept.conf.fail_test.js
new file mode 100644
index 00000000..c0af566a
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/codecept.conf.fail_test.js
@@ -0,0 +1,16 @@
+exports.config = {
+  tests: './*_ftest.js',
+  output: './output',
+  helpers: {
+    CustomHelper: {
+      require: './customHelper.js',
+    },
+  },
+  rerun: {
+    minSuccess: 2,
+    maxReruns: 3,
+  },
+  bootstrap: null,
+  mocha: {},
+  name: 'run-rerun',
+};
diff --git a/test/data/sandbox/configs/run-rerun/codecept.conf.js b/test/data/sandbox/configs/run-rerun/codecept.conf.js
new file mode 100644
index 00000000..43bab903
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/codecept.conf.js
@@ -0,0 +1,16 @@
+exports.config = {
+  tests: './*_test.js',
+  output: './output',
+  helpers: {
+    CustomHelper: {
+      require: './customHelper.js',
+    },
+  },
+  rerun: {
+    minSuccess: 3,
+    maxReruns: 3,
+  },
+  bootstrap: null,
+  mocha: {},
+  name: 'run-rerun',
+};
diff --git a/test/data/sandbox/configs/run-rerun/codecept.conf.min_less_max.js b/test/data/sandbox/configs/run-rerun/codecept.conf.min_less_max.js
new file mode 100644
index 00000000..99708807
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/codecept.conf.min_less_max.js
@@ -0,0 +1,16 @@
+exports.config = {
+  tests: './*_test.js',
+  output: './output',
+  helpers: {
+    CustomHelper: {
+      require: './customHelper.js',
+    },
+  },
+  rerun: {
+    minSuccess: 2,
+    maxReruns: 3,
+  },
+  bootstrap: null,
+  mocha: {},
+  name: 'run-rerun',
+};
diff --git a/test/data/sandbox/configs/run-rerun/codecept.conf.min_more_max.js b/test/data/sandbox/configs/run-rerun/codecept.conf.min_more_max.js
new file mode 100644
index 00000000..e60ac941
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/codecept.conf.min_more_max.js
@@ -0,0 +1,16 @@
+exports.config = {
+  tests: './*_test.js',
+  output: './output',
+  helpers: {
+    CustomHelper: {
+      require: './customHelper.js',
+    },
+  },
+  rerun: {
+    minSuccess: 4,
+    maxReruns: 3,
+  },
+  bootstrap: null,
+  mocha: {},
+  name: 'run-rerun',
+};
diff --git a/test/data/sandbox/configs/run-rerun/customHelper.js b/test/data/sandbox/configs/run-rerun/customHelper.js
new file mode 100644
index 00000000..7a45cc30
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/customHelper.js
@@ -0,0 +1,7 @@
+class CustomHelper extends Helper {
+  printMessage(s) {
+    console.log(s);
+  }
+}
+
+module.exports = CustomHelper;
diff --git a/test/data/sandbox/configs/run-rerun/first_ftest.js b/test/data/sandbox/configs/run-rerun/first_ftest.js
new file mode 100644
index 00000000..d385a54f
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/first_ftest.js
@@ -0,0 +1,17 @@
+/* eslint-disable radix */
+Feature('Run Rerun - Command');
+
+Scenario('@RunRerun - Fail all attempt', (I) => {
+  I.printMessage('RunRerun');
+  throw new Error('Test Error');
+});
+
+
+Scenario('@RunRerun - fail second test', (I) => {
+  I.printMessage('RunRerun');
+  process.env.FAIL_ATTEMPT = parseInt(process.env.FAIL_ATTEMPT) + 1;
+  console.log(process.env.FAIL_ATTEMPT);
+  if (process.env.FAIL_ATTEMPT === '2') {
+    throw new Error('Test Error');
+  }
+});
diff --git a/test/data/sandbox/configs/run-rerun/first_test.js b/test/data/sandbox/configs/run-rerun/first_test.js
new file mode 100644
index 00000000..450890a3
--- /dev/null
+++ b/test/data/sandbox/configs/run-rerun/first_test.js
@@ -0,0 +1,5 @@
+Feature('Run Rerun - Command');
+
+Scenario('@RunRerun', (I) => {
+  I.printMessage('RunRerun');
+});
diff --git a/test/data/sandbox/configs/todo/codecept.conf.js b/test/data/sandbox/configs/todo/codecept.conf.js
new file mode 100644
index 00000000..6b0ee181
--- /dev/null
+++ b/test/data/sandbox/configs/todo/codecept.conf.js
@@ -0,0 +1,7 @@
+exports.config = {
+  tests: './*_test.js',
+  output: './output',
+  bootstrap: null,
+  mocha: {},
+  name: 'todo-test',
+};
diff --git a/test/data/sandbox/configs/todo/first_test.js b/test/data/sandbox/configs/todo/first_test.js
new file mode 100644
index 00000000..eab440c4
--- /dev/null
+++ b/test/data/sandbox/configs/todo/first_test.js
@@ -0,0 +1,25 @@
+const { event } = require('../../../../../lib/index');
+
+Feature('Todo tests');
+
+event.dispatcher.on(event.suite.before, (suite) => {
+  for (const test of suite.tests) {
+    if (test.pending) {
+      console.log(`test ${test.title} was marked for todo with message: ${test.opts.skipInfo.message}`);
+      console.log(`test ${test.title} was marked for todo with customOpts: ${JSON.stringify(test.opts.customOpts)}`);
+    }
+  }
+});
+
+Scenario.todo('@todo', () => {
+  console.log('todo test not passed');
+});
+
+
+Scenario.todo('@todo without function');
+
+Scenario.todo('@todo with opts', { customOpts: 'Custom options for todo' });
+
+Scenario('@NotTodo', () => {
+  console.log('simple test was passed');
+});
diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js
index 62cf1511..69caee85 100644
--- a/test/helper/WebDriver_test.js
+++ b/test/helper/WebDriver_test.js
@@ -1129,4 +1129,14 @@ describe('WebDriver', function () {
       expect(height).is.greaterThan(0);
     });
   });
+
+  describe('#scrollIntoView', () => {
+    it('should scroll element into viewport', async () => {
+      await wd.amOnPage('/form/scroll_into_view');
+      const element = await wd.browser.$('#notInViewportByDefault');
+      expect(await element.isDisplayedInViewport()).to.be.false;
+      await wd.scrollIntoView('#notInViewportByDefault');
+      expect(await element.isDisplayedInViewport()).to.be.true;
+    });
+  });
 });
diff --git a/test/runner/run_rerun_test.js b/test/runner/run_rerun_test.js
new file mode 100644
index 00000000..1f2de0f3
--- /dev/null
+++ b/test/runner/run_rerun_test.js
@@ -0,0 +1,110 @@
+const assert = require('assert');
+const path = require('path');
+const expect = require('chai').expect;
+
+const exec = require('child_process').exec;
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/run-rerun/');
+const codecept_run = `${runner} run-rerun`;
+const codecept_run_config = (config, grep) => `${codecept_run} --config ${codecept_dir}/${config} --grep "${grep || ''}"`;
+
+describe('run-rerun command', () => {
+  before(() => {
+    process.chdir(codecept_dir);
+  });
+
+  it('should display count of attemps', (done) => {
+    exec(`${codecept_run_config('codecept.conf.js')} --debug`, (err, stdout) => {
+      stdout.should.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+    I print message "RunRerun"
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.include('Process run 1 of max 3, success runs 1/3 ');
+      stdout.should.include('Process run 2 of max 3, success runs 2/3 ');
+      stdout.should.include('Process run 3 of max 3, success runs 3/3 ');
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should display 2 success count of attemps', (done) => {
+    exec(`${codecept_run_config('codecept.conf.min_less_max.js')} --debug`, (err, stdout) => {
+      stdout.should.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.not.include(`
+Run Rerun - Command --
+  @RunRerun
+    I print message "RunRerun"
+    I print message "RunRerun"
+    I print message "RunRerun"
+RunRerun
+  ✔ OK`);
+      stdout.should.include('Process run 1 of max 3, success runs 1/2 ');
+      stdout.should.include('Process run 2 of max 3, success runs 2/2 ');
+      stdout.should.not.include('Process run 3 of max 3');
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should display error if minSuccess more then maxReruns', (done) => {
+    exec(`${codecept_run_config('codecept.conf.min_more_max.js')} --debug`, (err, stdout) => {
+      stdout.should.include('minSuccess must be less then maxReruns');
+      assert(err);
+      done();
+    });
+  });
+
+  it('should display errors if test is fail always', (done) => {
+    exec(`${codecept_run_config('codecept.conf.fail_test.js', '@RunRerun - Fail all attempt')} --debug`, (err, stdout) => {
+      stdout.should.include('Fail run 1 of max 3, success runs 0/2 ');
+      stdout.should.include('Fail run 2 of max 3, success runs 0/2 ');
+      stdout.should.include('Fail run 3 of max 3, success runs 0/2 ');
+      stdout.should.include('Test suite unstable!');
+      assert(err);
+      done();
+    });
+  });
+
+  it('should display success run if test was fail one time of two attepmts and 3 reruns', (done) => {
+    exec(`FAIL_ATTEMPT=0  ${codecept_run_config('codecept.conf.fail_test.js', '@RunRerun - fail second test')} --debug`, (err, stdout) => {
+      stdout.should.include('Process run 1 of max 3, success runs 1/2');
+      stdout.should.include('Fail run 2 of max 3, success runs 1/2');
+      stdout.should.include('Process run 3 of max 3, success runs 2/2');
+      stdout.should.not.include('Test suite unstable!');
+      assert(!err);
+      done();
+    });
+  });
+});
diff --git a/test/runner/todo_test.js b/test/runner/todo_test.js
new file mode 100644
index 00000000..4ca8a4e5
--- /dev/null
+++ b/test/runner/todo_test.js
@@ -0,0 +1,38 @@
+const path = require('path');
+const exec = require('child_process').exec;
+const assert = require('assert');
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/todo');
+const codecept_run = `${runner} run --config ${codecept_dir}/codecept.conf.js `;
+
+describe('Todo', () => {
+  it('should skip test with todo', (done) => {
+    exec(`${codecept_run}`, (err, stdout) => {
+      stdout.should.include('S @todo');
+      stdout.should.include('S @todo without function');
+      stdout.should.not.include('todo test not passed');
+      stdout.should.include('✔ @NotTodo in');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should skip inject skipinfo to todo test', (done) => {
+    exec(`${codecept_run}`, (err, stdout) => {
+      stdout.should.include('test @todo was marked for todo with message: Test not implemented!');
+      stdout.should.include('test @todo without function was marked for todo with message: Test not implemented!');
+      stdout.should.not.include('test @NotTodo was marked for todo with message: Test not implemented!');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should correctly pass custom opts for todo test', (done) => {
+    exec(`${codecept_run}`, (err, stdout) => {
+      stdout.should.include('test @todo with opts was marked for todo with customOpts: "Custom options for todo"');
+      assert(!err);
+      done();
+    });
+  });
+});
diff --git a/test/unit/ui_test.js b/test/unit/ui_test.js
index 3c59203c..a6b6886f 100644
--- a/test/unit/ui_test.js
+++ b/test/unit/ui_test.js
@@ -117,5 +117,35 @@ describe('ui', () => {
       scenarioConfig.injectDependencies({ Data: 'data' });
       assert.equal(scenarioConfig.test.inject.Data, 'data');
     });
+
+    describe('todo', () => {
+      it('should inject skipInfo to opts', () => {
+        scenarioConfig = context.Scenario.todo('scenario', () => { console.log('Scenario Body'); });
+
+        assert.equal(scenarioConfig.test.pending, true, 'Todo Scenario must be contain pending === true');
+        assert.equal(scenarioConfig.test.opts.skipInfo.message, 'Test not implemented!');
+        assert.equal(scenarioConfig.test.opts.skipInfo.description, "() => { console.log('Scenario Body'); }");
+      });
+
+      it('should contain empty description in skipInfo and empty body', () => {
+        scenarioConfig = context.Scenario.todo('scenario');
+
+        assert.equal(scenarioConfig.test.pending, true, 'Todo Scenario must be contain pending === true');
+        assert.equal(scenarioConfig.test.opts.skipInfo.description, '');
+        assert.equal(scenarioConfig.test.body, '');
+      });
+
+      it('should inject custom opts to opts and without callback', () => {
+        scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' });
+
+        assert.equal(scenarioConfig.test.opts.customOpts, 'Custom Opts');
+      });
+
+      it('should inject custom opts to opts and with callback', () => {
+        scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }, () => { console.log('Scenario Body'); });
+
+        assert.equal(scenarioConfig.test.opts.customOpts, 'Custom Opts');
+      });
+    });
   });
 });