Repository: Codeception/CodeceptJS
Pull request #993 (2018-03-08 19:12:20)

[WIP][feature] Introduce chunking to run-multiple
Reworked `run-multiple` command to support chunking by dynamical expanding the configuration. Also simplifies internal expansion by browsers.

- [x] Tests pass
- [x] No breaking changes
- [x] Documentation

**Changelog:**
- added documentation
- renamed: Suite -> Run
- renamed: runSuite -> executeRun
- renamed: preferredSuites -> selectedRuns
- added new class in run-multiple/collection
- added new class in run-multiple/run
- added new set of functions in run-multiple/chunk
- added: collection.prepareRuns - creates unified suites coniguration based on preferredSuites (preferred suites are either suites passed via --all or explicitly)
- added: collection.prepareChunks - expands configuration by (n) chunks if chunks are configured
- added: collection.prepareBrowsers - expands configuration by (n) browsers if browsers are configured
- added: collection.filterSelectedBrowsers - filters configuration by requested browsers set via selectedRuns
- added: chunking also supports a simple version of grep
- added: chunking also supports a callback instead of a number
- updated: runSuite -> executeRun
  - browser argument is removed, it is now retrieved through a unified browser config
  - forking over multiple browsers is removed, it became obsolete as soon as we prepared
    and expanded the whole configuration beforehand, including chunks and browsers

DavertMik: Btw, currently `suite` represents a test file. 
We even have events fired:

* event.suite.before(suite) - before a suite
* event.suite.after(suite) - after a suite

which handled with BeforeAll, AfterAll hooks. The suite object comes from Mocha.
If this file does not correspond to this type of a suite it should be renamed
sveneisenschmidt: @DavertMik I’d like to rename it to just ‘run’ and ‘collection‘.
DavertMik: @sveneisenschmidt any updates?
sveneisenschmidt: @DavertMik Still on it 🙂
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Issues
======
- Added 34
           

Complexity increasing per file
==============================
- lib/command/run-multiple.js  1
         
```


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


```diff

Issues
======
- Added 34
           

Complexity increasing per file
==============================
- lib/command/run-multiple.js  1
         
```


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


```diff

Complexity increasing per file
==============================
- lib/command/run-multiple.js  1
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1412708&bid=6739071)
        
sveneisenschmidt: @DavertMik @reubenmiller Please see my latest code changes and the updated Pull Request. :)
DavertMik: @APshenkin could you look into it as well?

Looks nice to me. Could you update documentation https://github.com/Codeception/CodeceptJS/blob/master/docs/advanced.md#multiple-execution with examples of splitting tests by chunks (by number or by function)
APshenkin: @DavertMik Looks good for me

@sveneisenschmidt  good work!
DavertMik: @sveneisenschmidt  So to finalize this we need some documentation added :)
DavertMik: >  Have you seen my thread message in Slack about the effectiveness of chunking?

Not sure. We had issues with `glob` usage and that's last I see in dev channel.
sveneisenschmidt: @DavertMik I‘ll give my best. Let’s do this! 
codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- lib/command/run-multiple.js  1
         
```


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


```diff

Complexity increasing per file
==============================
- lib/command/run-multiple.js  1
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1412708&bid=6739071)
        
DavertMik: Looks awesome, thanks. I will merge it when I push my own changes prepared for 1.2 version

Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index fc720e62..93b47a3e 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -24,7 +24,7 @@ program.command('list [path]')
   .action(require('../lib/command/list'));
 
 program.command('def [path]')
-  .description('List all actions for I.')
+  .description('Generates TypeScript definitions for all I actions.')
   .option('-c, --config [file]', 'configuration file to be used')
   .action(require('../lib/command/definitions'));
 
diff --git a/docs/acceptance.md b/docs/acceptance.md
index c4392c12..2a2e7469 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -263,6 +263,68 @@ within({frame: [".content", "#editor"]}, () => {
 });
 ```
 
+## Multiple Sessions
+
+*Note: Currently multiple sessions works only in WebDriverIO and Protractor*
+
+CodeceptJS allows to run several browser sessions inside a test. This can be useful for testing communication between users inside a system, for instance in chats. To open another browser use `session()` function as shown in example:
+
+```js
+Scenario('test app', (I) => {
+  I.amOnPage('/chat');
+  I.fillField('name', 'davert');
+  I.click('Sign In');
+  I.see('Hello, davert');
+  session('john', () => {
+    // another session started
+    I.amOnPage('/chat');
+    I.fillField('name', 'john');
+    I.click('Sign In');
+    I.see('Hello, john');
+  });
+  // switching back to default session
+  I.fillField('message', 'Hi, john');
+  // there is a message from current user
+  I.see('me: Hi, john', '.messages');
+  session('john', () => {
+    // let's check if john received it
+    I.see('davert: Hi, john', '.messages');
+  });
+});
+```
+
+`session` function expects a first parameter to be a name of a session. You can switch back to session by using the same name.
+
+You can override config for session by passing second parameter:
+
+```js
+session('john', { browser: 'firefox' } , () => {
+  // run this steps in firefox
+  I.amOnPage('/');
+});
+```
+
+or just start session without switching to it. Call `session` passing only its name:
+
+```js
+Scenario('test', (I) => {
+  // opens 3 additional browsers
+  session('john');
+  session('mary');
+  session('jane');
+
+  I.amOnPage('/');
+
+  // switch to session by its name
+  session('mary', () => {
+    I.amOnPage('/login');
+  });
+}
+```
+
+Function passed into session can use `I`, page objects, and any objects declared for the scenario.
+This funtion can also be declared as async (but doesn't work as generator). You can use `within` inside a session but you can't call session from inside `within`.
+
 ---
 
 ### done()
diff --git a/docs/advanced.md b/docs/advanced.md
index 8499feed..9c1ef7e4 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -87,7 +87,13 @@ It can be turned on with `--debug` flag.
 codeceptjs run --debug
 ```
 
-While running in debug mode you can pause execution and enter interactive console mode by using `pause()` function.
+to receive even more information turn on `--verbose` flag:
+
+```sh
+codeceptjs run --verbose
+```
+
+And don't forget that you can pause execution and enter **interactive console** mode by calling `pause()` inside your test.
 
 For advanced debugging use NodeJS debugger. In WebStorm IDE:
 
@@ -97,7 +103,7 @@ node $NODE_DEBUG_OPTION ./node_modules/.bin/codeceptjs run
 
 ## Multiple Execution
 
-CodeceptJS can execute multiple suites in parallel. This is useful if you want to execute same tests but on different browsers and with different configurations. Before using this feature you need to add `multiple` option to the config:
+CodeceptJS can execute multiple suites in parallel. This is useful if you want to execute same tests but on different browsers and with different configurations or different tests on same browsers in parallel. Before using this feature you need to add `multiple` option to the config:
 
 ```js
 "multiple": {
@@ -120,7 +126,15 @@ CodeceptJS can execute multiple suites in parallel. This is useful if you want t
       // replace any config values from WebDriverIO helper
       {"browser": "chrome", "windowSize": "1200x840"}
     ]
-  }
+  },
+
+  "parallel": {
+    // Splits tests into chunks
+    "chunks": 2,
+    // run all tests in chrome
+    "browsers": ["chrome"]
+  },
+
 }
 ```
 
@@ -183,5 +197,43 @@ Output is printed for all running processes. Each line is tagged with a suite an
 
 Hooks are available when using the `run-multiple` command to perform actions before the test suites start and after the test suites have finished. See [Hooks](http://codecept.io/hooks/#bootstrap-teardown) for an example.
 
+### Chunks
+
+Chunking is a way to execute multiple tests in parallel. The default chunking collects all test files and executes them in parallel by the specified amount of chunks. Given we have five test scenarios (`a_test.js`,`b_test.js`,`c_test.js`,`d_test.js` and `e_test.js`), by setting `"chunks": 2` we tell the runner to run two suites in parallel. The first suite will run `a_test.js`,`b_test.js` and `c_test.js`, the second suite will run `d_test.js` and `e_test.js`. 
+
+Grep and multiple browsers are supported. Passing more than one browser will multiply the amount of suites by the amount of browsers passed. The following example will lead to four parallel runs.
+
+```js
+"multiple": {
+  // 2x chunks + 2x browsers = 4
+  "parallel": {
+    // Splits tests into chunks
+    "chunks": 2,
+    // run all tests in chrome and firefox
+    "browsers": ["chrome", "firefox"] 
+  },
+}
+```
+
+Passing a function will enable you to provide your own chunking algorithm. The first argument passed to you function is an array of all test files, if you enabled grep the test files passed are already filtered to match the grep pattern.
+
+```js
+"multiple": {
+  "parallel": {
+    // Splits tests into chunks by passing an anonymous function,
+    // only execute first and last found test file
+    "chunks": (files) => {
+      return [
+        [ files[0] ], // chunk 1
+        [ files[files.length-1] ], // chunk 2
+      ]  
+    },
+    // run all tests in chrome and firefox
+    "browsers": ["chrome", "firefox"]
+  },
+}
+```
+
+Note: Chunking will be most effective if you have many individual test files that contain only a small amount of scenarios. Otherwise the combined execution time of many scenarios or big scenarios in one single test file potentially lead to an uneven execution time.
 
 ## done()
diff --git a/docs/angular.md b/docs/angular.md
index a3e9afc4..b273a707 100644
--- a/docs/angular.md
+++ b/docs/angular.md
@@ -123,7 +123,22 @@ You can also run `list` command to see methods of I:
 codeceptjs list
 ```
 
-### Writing First Test
+## Testing non-Angular Applications
+
+Sure, Protractor can be used to test applications built without AngularJS. In this case you need to disable angular synchronization feature in config:
+
+```js
+"helpers": {
+  "Protractor": {
+    "url": "http://todomvc.com/examples/angularjs/",
+    "driver": "hosted",
+    "browser": "firefox",
+    "angular": false
+  }
+}
+```
+
+## Writing First Test
 
 Test scenario should always use `I` object to execute commands.
 This is important as all methods of `I` are running in global promise chain, this way CodeceptJS makes sure everything is executed in right order.
@@ -154,7 +169,7 @@ A test can be executed with `run` command, we recommend to use `--steps` options
 ```sh
 $ codeceptjs run --steps
 
-CodeceptJS v0.3.2
+CodeceptJS 1.2.0
 Test root is assumed to be /home/davert/demos/todoangular
 Using the selenium server at http://localhost:4444/wd/hub
 
@@ -164,7 +179,7 @@ TodoMvc --
  • I dont see element "#todo-count"
 ```
 
-### Running Several Scenarios
+## Running Several Scenarios
 
 By writing a test in similar manner we will have a test shown in the beginning of this guide. Probably we would like not to finish with one test,
 but have more, for testing editing of todo items, checking todo items, and more.
@@ -179,7 +194,7 @@ Before((I) => {
   I.amOnPage('/');
 });
 
-var createTodo = function (I, name) {
+const createTodo = function (I, name) {
   I.fillField({model: 'newTodo'}, name);
   I.pressKey('Enter');
 }
@@ -213,7 +228,7 @@ Scenario('check todo item', (I) => {
 });
 ```
 
-### Locators
+## Locators
 
 Like you may have noticed, CodeceptJS doesn't use `by.*` locators similar to Protractor or Selenium Webdriver.
 Instead most of methods expect you to pass valid CSS or XPath. In case you don't want CodeceptJS to guess the type of locator,
@@ -232,13 +247,12 @@ Same thing happens for form fields: they are searched by field names, labels, an
 
 Using such smart locators makes tests easy to write, however, searching an element by text is slower than using CSS|XPath and much slower than using strict locators.
 
-### Refactoring
+## Refactoring
 
 In previous examples, we've moved actions into `createTodo` function. Is there a more elegant way of refactoring?
 Can we have something like `I.createTodo()` to be used in code? Sure, we can do so by editing `steps_file.js` created by init command.
 
 ```js
-'use strict';
 // in this file you can append custom step methods to 'I' object
 
 module.exports = function() {
@@ -264,22 +278,11 @@ Scenario('create todo item', (I) => {
 
 To learn more about refactoring options in CodeceptJS read [PageObjects guide](http://codecept.io/pageobjects/).
 
-### Testing non-Angular Applications
-
-Sure, Protractor can be used to test applications built without AngularJS. In this case you need to disable angular synchronization feature in config:
+## Multiple Sessions
 
-```js
-"helpers": {
-  "Protractor": {
-    "url": "http://todomvc.com/examples/angularjs/",
-    "driver": "hosted",
-    "browser": "firefox",
-    "angular": false
-  }
-}
-```
+To run several browsers in one test see [multiple session](https://codecept.io/acceptance/#multiple-sessions) chapter.
 
-### Extending
+## Extending
 
 What if CodeceptJS doesn't provide some of Protractor functionality you actually need? Sure its API is to general,
 and this case is possible. If you don't know how to do something with CodeceptJS - revert back to Protractor syntax!
diff --git a/docs/basics.md b/docs/basics.md
index 015b6bab..a994c9ec 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -189,13 +189,18 @@ Scenario('My scenario', {key: val}, (I) => {});
 By default there is no timeout for tests, however you can change this value for a specific suite:
 
 ```js
-Feature('Stop me', {timeout: 5000}); // set timeout to 5s
+Feature('Stop me').timeout(5000); // set timeout to 5s
 ```
 
 or for the test:
 
 ```js
 // set timeout to 1s
+Scenario("Stop me faster", (I) => {
+  // test goes here
+}).timeout(1000);
+
+// alternative
 Scenario("Stop me faster", {timeout: 1000}, (I) => {});
 
 // disable timeout for this scenario
@@ -213,12 +218,7 @@ CodeceptJS implements retries the same way [Mocha do](https://mochajs.org#retry-
 You can set number of a retries for a feature:
 
 ```js
-Feature('Complex JS Stuff', {retries: 3})
-
-
-Scenario('Really complex', (I) => {
-  // test goes here
-});
+Feature('Complex JS Stuff').retry(3);
 ```
 
 Every Scenario inside this feature will be rerun 3 times.
@@ -227,10 +227,12 @@ You can make an exception for a specific scenario by passing `retries` option to
 #### Retry Scenario
 
 ```js
-Scenario('Really complex', {retries: 2}, (I) => {
+Scenario('Really complex', (I) => {
   // test goes here
-});
+}).retry(2);
 
+// alternative
+Scenario('Really complex', { retries: 2 }, (I) => {});
 ```
 
 This scenario will be restarted two times on a failure
diff --git a/docs/docker.md b/docs/docker.md
index 3e079107..ded9bed6 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -92,7 +92,7 @@ docker build -t codeception/codeceptjs .
 
 * this directory will be added as `/codecept` insde container
 * tests directory is expected to be mounted as `/tests`
-* `codeceptjs` is a synlink to `/codecept/bin/codecept.js`
+* `codeceptjs` is a symlink to `/codecept/bin/codecept.js`
 
 ### Passing Options
 
diff --git a/docs/hooks.md b/docs/hooks.md
index fa44e485..a3b5c78f 100644
--- a/docs/hooks.md
+++ b/docs/hooks.md
@@ -235,20 +235,27 @@ module.exports = function() {
 
 Available events:
 
-* `event.test.started(test)` - at the very beginning of a test. Passes a current test object.
-* `event.test.before` - when `Before` hooks from helpers and from test is executed
-* `event.test.after` - after each test
-* `event.test.passed(test)` - when test passed
-* `event.test.failed(test, error)` - when test failed
-* `event.suite.before(suite)` - before a suite
-* `event.suite.after(suite)` - after a suite
-* `event.step.started(step)` - when step hooks from helpers executed. Passes current step object.
-* `event.step.before` - at the very beginning of a step
-* `event.step.after`- after a step
+* `event.test.before` - *async* when `Before` hooks from helpers and from test is executed
+* `event.test.after` - *async* after each test
+* `event.test.started(test)` - *sync* at the very beginning of a test. Passes a current test object.
+* `event.test.passed(test)` - *sync* when test passed
+* `event.test.failed(test, error)` - *sync* when test failed
+* `event.test.finished(test)` - *sync* when test finished
+* `event.suite.before(suite)` - *async* before a suite
+* `event.suite.after(suite)` - *async* after a suite
+* `event.step.before` - *async* when the step is scheduled for execution
+* `event.step.after`- *async* after a step
+* `event.step.started(step)` - *sync* when step starts.
+* `event.step.passed(step)` - *sync* when step passed.
+* `event.step.failed(step, err)` - *sync* when step failed.
+* `event.step.finished(step)` - *sync* when step finishes.
 * `event.all.before` - before running tests
 * `event.all.after` - after running tests
 * `event.all.result` - when results are printed
 
+* *sync* - means that event is fired in the moment of action happens.
+* *async* - means that event is fired when an actions is scheduled. Use `recorder` to schedule your actions.
+
 For further reference look for [currently available listeners](https://github.com/Codeception/CodeceptJS/tree/master/lib/listener) using event system.
 
 #### Test Object
diff --git a/lib/actor.js b/lib/actor.js
index 5ada9adb..af1206e2 100644
--- a/lib/actor.js
+++ b/lib/actor.js
@@ -73,12 +73,14 @@ function recordStep(step, args) {
   recorder.add('step passed', () => {
     step.endTime = Date.now();
     event.emit(event.step.passed, step);
+    event.emit(event.step.finished, step);
   });
 
   recorder.catchWithoutStop((err) => {
     step.status = 'failed';
     step.endTime = Date.now();
     event.emit(event.step.failed, step);
+    event.emit(event.step.finished, step);
     throw err;
   });
 
diff --git a/lib/assert/throws.js b/lib/assert/throws.js
new file mode 100644
index 00000000..c5b97f00
--- /dev/null
+++ b/lib/assert/throws.js
@@ -0,0 +1,20 @@
+function errorThrown(actual, expected) {
+  if (!expected) return null;
+  if (!actual) throw new Error(`Expected ${expected} error to be thrown`);
+  const msg = actual.inspect ? actual.inspect() : actual.toString();
+  if (expected instanceof RegExp) {
+    if (msg.match(expected)) return null;
+    throw new Error(`Expected error to be thrown with message matching ${expected} while '${msg}' caught`);
+  }
+  if (typeof expected === 'string') {
+    if (msg === expected) return null;
+    throw new Error(`Expected error to be thrown with message ${expected} while '${msg}' caught`);
+  }
+  if (typeof expected === 'object') {
+    if (actual.constructor.name !== expected.constructor.name) throw new Error(`Expected ${expected} error to be thrown but ${actual} was caught`);
+    if (expected.message && expected.message !== msg) throw new Error(`Expected error to be thrown with message ${expected.message} while '${msg}' caught`);
+  }
+  return null;
+}
+
+module.exports = errorThrown;
diff --git a/lib/codecept.js b/lib/codecept.js
index c54ccaa1..c35dcff3 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -40,6 +40,7 @@ class Codecept {
       global.Helper = global.codecept_helper = require('./helper');
       global.pause = require('./pause');
       global.within = require('./within');
+      global.session = require('./session');
       global.DataTable = require('./data/table');
     }
 
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index ad18410e..a6ecff7b 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -9,23 +9,24 @@ const output = require('../output');
 const path = require('path');
 const runHook = require('../hooks');
 const event = require('../event');
+const collection = require('./run-multiple/collection');
 
 const runner = path.join(__dirname, '/../../bin/codecept');
 let config;
 const childOpts = {};
 const copyOptions = ['steps', 'reporter', 'verbose', 'config', 'reporter-options', 'grep', 'fgrep', 'debug'];
 
-// codeceptjs run:multiple smoke:chrome regression:firefox - will launch smoke suite in chrome and regression in firefox
-// codeceptjs run:multiple smoke:chrome regression - will launch smoke suite in chrome and regression in firefox and chrome
-// codeceptjs run:multiple all - will launch all suites
+// codeceptjs run:multiple smoke:chrome regression:firefox - will launch smoke run in chrome and regression in firefox
+// codeceptjs run:multiple smoke:chrome regression - will launch smoke run in chrome and regression in firefox and chrome
+// codeceptjs run:multiple all - will launch all runs
 // codeceptjs run:multiple smoke regression'
 
-let suiteId = 1;
+let runId = 1;
 let subprocessCount = 0;
 let totalSubprocessCount = 0;
 let processesDone;
 
-module.exports = function (suites, options) {
+module.exports = function (selectedRuns, options) {
   // registering options globally to use in config
   process.profile = options.profile;
   const configFile = options.config;
@@ -34,12 +35,6 @@ module.exports = function (suites, options) {
   const testRoot = getTestRoot(configFile);
   config = getConfig(configFile);
 
-  const configMultiple = config.multiple;
-
-  if (!configMultiple) {
-    fail('Multiple suites not configured, add "multiple": { /../ } section to config');
-  }
-
   // copy opts to run
   Object.keys(options)
     .filter(key => copyOptions.indexOf(key) > -1)
@@ -47,11 +42,13 @@ module.exports = function (suites, options) {
       childOpts[key] = options[key];
     });
 
-  if (options.all) {
-    suites = Object.keys(configMultiple);
+  if (!config.multiple) {
+    fail('Multiple runs not configured, add "multiple": { /../ } section to config');
   }
-  if (!suites.length) {
-    fail('No suites provided. Use --all option to run all configured suites');
+
+  selectedRuns = options.all ? Object.keys(config.multiple) : selectedRuns;
+  if (!selectedRuns.length) {
+    fail('No runs provided. Use --all option to run all configured runs');
   }
 
   const done = () => event.emit(event.multiple.before, null);
@@ -61,43 +58,16 @@ module.exports = function (suites, options) {
     processesDone = resolve;
   });
 
-  const forksToExecute = [];
-  // iterate options
-  suites.forEach((suite) => {
-    // get suites
-    suite = suite.split(':');
-
-    const suiteName = suite[0];
-    const browser = suite[1];
-    const suiteConf = configMultiple[suiteName];
-
-    if (!suiteConf) {
-      throw new Error(`Suite ${suiteName} was not configured in "multiple" section of config`);
-    }
-
-    // filter run configurations by browser if exists
-    if (browser) {
-      suiteConf.browsers = suiteConf.browsers.filter((b) => {
-        if (typeof b === 'object') {
-          return b.browser === browser;
-        }
-        return b === browser;
-      });
-    }
-    // throw error if no configuration
-    if (suiteConf.browsers.length === 0) throw new Error(`Browser ${browser} not found in multiple suite "${suiteName}" config`);
-
-    const suiteForks = runSuite(suiteName, suiteConf);
-    if (Array.isArray(suiteForks)) {
-      forksToExecute.push(...suiteForks);
-    } else {
-      forksToExecute.push(suiteForks);
-    }
+  const runsToExecute = [];
+  collection.createRuns(selectedRuns, config).forEach((run) => {
+    const runName = run.getOriginalName() || run.getName();
+    const runConfig = run.getConfig();
+    runsToExecute.push(executeRun(runName, runConfig));
   });
 
   // Execute all forks
-  totalSubprocessCount = forksToExecute.length;
-  forksToExecute.forEach(currentForkFunc => currentForkFunc.call(this));
+  totalSubprocessCount = runsToExecute.length;
+  runsToExecute.forEach(runToExecute => runToExecute.call(this));
 
   return childProcessesPromise.then(() => {
     // fire hook
@@ -106,51 +76,39 @@ module.exports = function (suites, options) {
   });
 };
 
-function runSuite(suite, suiteConf, browser) {
-  if (!browser) {
-    // run configurations
-    return suiteConf.browsers.map((b) => {
-      const browserConf = Object.assign({}, suiteConf);
-      if (typeof b === 'object') {
-        browserConf.browser = b;
-        return runSuite(suite, browserConf, b.browser);
-      }
-      browserConf.browser = { browser: b };
-      return runSuite(suite, browserConf, b);
-    });
-  }
-
+function executeRun(runName, runConfig) {
   // clone config
   let overriddenConfig = Object.assign({}, config);
 
   // get configuration
-  const browserConfig = suiteConf.browser;
+  const browserConfig = runConfig.browser;
+  const browserName = browserConfig.browser;
 
   for (const key in browserConfig) {
     overriddenConfig.helpers = replaceValue(overriddenConfig.helpers, key, browserConfig[key]);
   }
 
-  let outputDir = `${suite}_`;
+  let outputDir = `${runName}_`;
   if (browserConfig.outputName) {
     outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
   } else {
     outputDir += JSON.stringify(browserConfig).replace(/[^\d\w]+/g, '_');
   }
-  outputDir += `_${suiteId}`;
+  outputDir += `_${runId}`;
 
   // tweaking default output directories and for mochawesome
   overriddenConfig = replaceValue(overriddenConfig, 'output', path.join(config.output, outputDir));
   overriddenConfig = replaceValue(overriddenConfig, 'reportDir', path.join(config.output, outputDir));
-  overriddenConfig = replaceValue(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browser}_report.xml`));
+  overriddenConfig = replaceValue(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browserName}_report.xml`));
 
   // override tests configuration
   if (overriddenConfig.tests) {
-    overriddenConfig.tests = suiteConf.tests;
+    overriddenConfig.tests = runConfig.tests;
   }
 
   // override grep param and collect all params
   const params = ['run',
-    '--child', `${suiteId++}.${suite}:${browser}`,
+    '--child', `${runId++}.${runName}:${browserName}`,
     '--override', JSON.stringify(overriddenConfig),
   ];
 
@@ -159,9 +117,9 @@ function runSuite(suite, suiteConf, browser) {
     if (childOpts[key] !== true) params.push(childOpts[key]);
   });
 
-  if (suiteConf.grep) {
+  if (runConfig.grep) {
     params.push('--grep');
-    params.push(suiteConf.grep);
+    params.push(runConfig.grep);
   }
 
   const onProcessEnd = (errorCode) => {
diff --git a/lib/command/run-multiple/chunk.js b/lib/command/run-multiple/chunk.js
new file mode 100644
index 00000000..4f59c5fb
--- /dev/null
+++ b/lib/command/run-multiple/chunk.js
@@ -0,0 +1,81 @@
+const glob = require('glob');
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * Splits a list to (n) parts, defined via the size argument.
+ */
+const splitFiles = (list, size) => {
+  const sets = [];
+  const chunks = list.length / size;
+  let i = 0;
+
+  while (i < chunks) {
+    sets[i] = list.splice(0, size);
+    i++;
+  }
+
+  return sets;
+};
+
+/**
+ * Executes a glob pattern and pushes the results to a list.
+ */
+const findFiles = (pattern) => {
+  const files = [];
+
+  glob.sync(pattern).forEach((file) => {
+    files.push(path.resolve(file));
+  });
+
+  return files;
+};
+
+/**
+ * Joins a list of files to a valid glob pattern
+ */
+const flattenFiles = (list) => {
+  const pattern = list.join(',');
+  return pattern.indexOf(',') > -1 ? `{${pattern}}` : pattern;
+};
+
+/**
+ * Greps a file by its content, checks if Scenario or Feature text'
+ * matches the grep text.
+ */
+const grepFile = (file, grep) => {
+  const contents = fs.readFileSync(file);
+  const pattern = new RegExp(`((Scenario|Feature)\(.*${grep}.*\))`, 'g'); // <- How future proof/solid is this?
+  return !!pattern.exec(contents);
+};
+
+/**
+ * Creates a list of chunks incl. configuration by either dividing a list of scenario
+ * files by the passed number or executing a usder deifned function to perform
+ * the splitting.
+ */
+const createChunks = (config, pattern) => {
+  const files = findFiles(pattern).filter((file) => {
+    return config.grep ? grepFile(file, config.grep) : true;
+  });
+
+  let chunks = [];
+  if (typeof config.chunks === 'function') {
+    chunks = config.chunks.call(this, [files]);
+  } else if (typeof config.chunks === 'number') {
+    chunks = splitFiles(files, Math.ceil(files.length / config.chunks));
+  } else {
+    throw new Error('chunks is neither a finite number or a valid function');
+  }
+
+  const chunkConfig = { ...config };
+  delete chunkConfig.chunks;
+
+  return chunks.map((chunkFiles) => {
+    return { ...chunkConfig, tests: flattenFiles(chunkFiles) };
+  });
+};
+
+module.exports = {
+  createChunks,
+};
diff --git a/lib/command/run-multiple/collection.js b/lib/command/run-multiple/collection.js
new file mode 100644
index 00000000..eb9c6b74
--- /dev/null
+++ b/lib/command/run-multiple/collection.js
@@ -0,0 +1,158 @@
+const { createChunks } = require('./chunk');
+const { createRun } = require('./run');
+
+/**
+ * Bootstraps a collection of runs, it combines user defined selection of runs
+ * to be executed and retrieves the run-specific configuration
+ */
+const createRuns = (selectedRuns, config) => {
+  return createCollection(config)
+    .prepareRuns(selectedRuns, config)
+    .prepareChunks(config)
+    .prepareBrowsers(config)
+    .filterSelectedBrowsers(selectedRuns, config)
+    .getRuns();
+};
+
+class Collection {
+  constructor(config) {
+    this.config = config;
+    this.runs = [];
+  }
+
+  addRun(run) {
+    this.runs.push(run);
+  }
+
+  removeRun(name) {
+    this.runs.forEach((run, index) => {
+      if (run.getName() === name) {
+        delete this.runs[index];
+        return true;
+      }
+    });
+
+    // Remove empty items
+    this.runs = this.runs.filter(r => !!r);
+
+    return false;
+  }
+
+  getRuns() {
+    return this.runs;
+  }
+
+  /**
+   * Expands the list of selected runs to runs incl. configuration from config.multiple.
+   */
+  prepareRuns(selectedRuns, config) {
+    selectedRuns.forEach((selectedRun) => {
+      const [runName] = selectedRun.split(':');
+      const runConfig = config.multiple[runName];
+
+      if (!runConfig) {
+        throw new Error(`run ${runName} was not configured in "multiple" section of config`);
+      }
+
+      this.addRun(createRun(runName, runConfig));
+    });
+
+    return this;
+  }
+
+  /**
+   * Expands runs via the `chunks` property to multiple runs. If config.chunks is a numeric
+   * value the list of scenario files is divided by this number. If config.chunks is a function,
+   * the function with the list of scenario files itself is called.
+   */
+  prepareChunks(config) {
+    this.runs.forEach((run) => {
+      const runName = run.getName();
+      const runConfig = run.getConfig();
+      const pattern = run.tests || config.tests;
+
+      if (!runConfig.chunks || !pattern) {
+        return;
+      }
+
+      createChunks(runConfig, pattern).forEach((runChunkConfig, index) => {
+        const run = createRun(`${runName}:chunk${index + 1}`, runChunkConfig);
+        run.setOriginalName(runName);
+        this.addRun(run);
+      });
+
+      this.removeRun(runName);
+    });
+
+    return this;
+  }
+
+  /**
+   * Expands browser declared via `browsers` property to multiple
+   * runs that all have one single `browser` property and omits
+   * the `browsers` property.
+   * */
+  prepareBrowsers(config) {
+    this.runs.forEach((run) => {
+      const runName = run.getName();
+      const runConfig = run.getConfig();
+      const browsers = [];
+
+      runConfig.browsers.forEach((browser) => {
+        const browserConfig = browser.browser ? browser : { browser };
+        const runBrowserConfig = { ...runConfig, browser: browserConfig };
+        browsers.push(browserConfig.browser);
+        const count = browsers.filter((b) => {
+          return b === browserConfig.browser;
+        }).length;
+
+        delete runBrowserConfig.browsers;
+
+        const run = createRun(`${runName}:${browserConfig.browser}${count}`, runBrowserConfig);
+        run.setOriginalName(runName);
+        this.addRun(run);
+      });
+
+      this.removeRun(runName);
+    });
+
+    return this;
+  }
+
+  /**
+   * Filters all runs by their `browser` property. The property `browsers` is ignored.
+   * If value of property `browser` does not match the slected `browser` in conjugation
+   * with the selectedRunName then the suite is removed from configuration.
+   */
+  filterSelectedBrowsers(selectedRuns, config) {
+    selectedRuns.forEach((selectedRun) => {
+      const [selectedRunName, selectedRunBrowserName] = selectedRun.split(':');
+
+      selectedRuns.forEach((selectedRun) => {
+        const [selectedRunName, selectedRunBrowserName] = selectedRun.split(':');
+        this.runs.forEach((run) => {
+          const runName = run.getName();
+          const runConfig = run.getConfig();
+
+          const runBrowserName = runConfig.browser.browser;
+
+          if (selectedRunBrowserName) {
+            if (selectedRunName === run.getOriginalName() && selectedRunBrowserName !== runBrowserName) {
+              this.removeRun(runName);
+            }
+          }
+        });
+      });
+    });
+
+    return this;
+  }
+}
+
+const createCollection = (runs) => {
+  return new Collection(runs);
+};
+
+module.exports = {
+  createRuns,
+};
diff --git a/lib/command/run-multiple/run.js b/lib/command/run-multiple/run.js
new file mode 100644
index 00000000..ea06309d
--- /dev/null
+++ b/lib/command/run-multiple/run.js
@@ -0,0 +1,30 @@
+class Run {
+  constructor(name, config) {
+    this.name = name;
+    this.config = config;
+  }
+
+  setOriginalName(originalName) {
+    this.originalName = originalName;
+  }
+
+  getOriginalName() {
+    return this.originalName;
+  }
+
+  getName() {
+    return this.name;
+  }
+
+  getConfig() {
+    return this.config;
+  }
+}
+
+const createRun = (name, config) => {
+  return new Run(name, config);
+};
+
+module.exports = {
+  createRun,
+};
diff --git a/lib/data/context.js b/lib/data/context.js
index c40c7981..d1f9d5fa 100644
--- a/lib/data/context.js
+++ b/lib/data/context.js
@@ -15,8 +15,8 @@ module.exports = function (context) {
             context.xScenario(`${title} | ${dataRow.data}`);
           } else {
             opts.data = dataRow;
-            const test = context.Scenario(`${title} | ${dataRow.data}`, opts, fn);
-            test.inject.current = dataRow.data;
+            context.Scenario(`${title} | ${dataRow.data}`, opts, fn)
+              .inject({ current: dataRow.data });
           }
         });
       },
@@ -31,8 +31,8 @@ module.exports = function (context) {
               context.xScenario(`${title} | ${dataRow.data}`);
             } else {
               opts.data = dataRow;
-              const test = context.Scenario.only(`${title} | ${dataRow.data}`, opts, fn);
-              test.inject.current = dataRow.data;
+              context.Scenario.only(`${title} | ${dataRow.data}`, opts, fn)
+                .inject({ current: dataRow.data });
             }
           });
         },
diff --git a/lib/event.js b/lib/event.js
index ef7c488f..36cf593c 100644
--- a/lib/event.js
+++ b/lib/event.js
@@ -11,6 +11,7 @@ module.exports = {
     after: 'test.after', // async
     passed: 'test.passed', // sync
     failed: 'test.failed', // sync
+    finished: 'test.finish', // sync
   },
   suite: {
     before: 'suite.before',
@@ -26,6 +27,7 @@ module.exports = {
     started: 'step.start', // sync
     passed: 'step.passed', // sync
     failed: 'step.failed', // sync
+    finished: 'step.finish', // sync
   },
   all: {
     before: 'global.before',
diff --git a/lib/helper.js b/lib/helper.js
index dd6ad9d1..a44e0e6c 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -26,6 +26,21 @@ class Helper {
 
   }
 
+  /**
+   * Abstract method to validate config
+   * @param {*} config
+   */
+  _validateConfig(config) {
+    return config;
+  }
+
+  /**
+   * Sets config for current test
+   */
+  _setConfig(opts) {
+    this.options = this._validateConfig(opts);
+  }
+
   /**
    * Hook executed before all tests
    */
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index cba230b8..3a48049f 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -106,7 +106,7 @@ class Appium extends WebdriverIO {
     }
 
     // set defaults
-    this.options = {
+    const defaults = {
       waitForTimeout: 1000, // ms
       port: 4723,
       host: 'localhost',
@@ -120,17 +120,17 @@ class Appium extends WebdriverIO {
     };
 
     // override defaults with config
-    this.options = Object.assign(this.options, config);
+    config = Object.assign(defaults, config);
 
-    this.options.baseUrl = this.options.url || this.options.baseUrl;
-    this.options.desiredCapabilities.deviceName = this.options.device || this.options.desiredCapabilities.deviceName;
-    this.options.desiredCapabilities.browserName = this.options.browser || this.options.desiredCapabilities.browserName;
-    this.options.desiredCapabilities.app = this.options.app || this.options.desiredCapabilities.app;
-    this.options.desiredCapabilities.platformName = this.options.platform || this.options.desiredCapabilities.platformName;
-    this.options.waitForTimeout /= 1000; // convert to seconds
+    config.baseUrl = config.url || config.baseUrl;
+    config.desiredCapabilities.deviceName = config.device || config.desiredCapabilities.deviceName;
+    config.desiredCapabilities.browserName = config.browser || config.desiredCapabilities.browserName;
+    config.desiredCapabilities.app = config.app || config.desiredCapabilities.app;
+    config.desiredCapabilities.platformName = config.platform || config.desiredCapabilities.platformName;
+    config.waitForTimeout /= 1000; // convert to seconds
 
 
-    if (!this.options.app && this.options.desiredCapabilities.browserName) {
+    if (!config.app && config.desiredCapabilities.browserName) {
       this.isWeb = true;
       this.root = webRoot;
     } else {
@@ -139,9 +139,11 @@ class Appium extends WebdriverIO {
     }
 
     this.platform = null;
-    if (this.options.desiredCapabilities.platformName) {
-      this.platform = this.options.desiredCapabilities.platformName.toLowerCase();
+    if (config.desiredCapabilities.platformName) {
+      this.platform = config.desiredCapabilities.platformName.toLowerCase();
     }
+
+    return config;
   }
 
   static _config() {
@@ -265,7 +267,7 @@ class Appium extends WebdriverIO {
   }
 
   /**
-   * Execute code only on iOS
+   * Execute code only on Android
    *
    * ```js
    * I.runOnAndroid(() => {
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index 3ab9cfcc..7f03aa3d 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -51,8 +51,14 @@ class Nightmare extends Helper {
   constructor(config) {
     super(config);
 
-    // set defaults
-    this.options = {
+    this.isRunning = false;
+
+    // override defaults with config
+    this._setConfig(config);
+  }
+
+  _validateConfig(config) {
+    const defaults = {
       waitForAction: 500,
       waitForTimeout: 1000,
       fullPageScreenshots: true,
@@ -66,12 +72,7 @@ class Nightmare extends Helper {
       enableHAR: false,
     };
 
-    this.isRunning = false;
-
-    // override defaults with config
-    Object.assign(this.options, config);
-
-    this.context = this.options.rootElement;
+    return Object.assign(defaults, config);
   }
 
   static _config() {
@@ -274,6 +275,7 @@ class Nightmare extends Helper {
   }
 
   async _startBrowser() {
+    this.context = this.options.rootElement;
     if (this.options.enableHAR) {
       this.browser = this.Nightmare(Object.assign(require('nightmare-har-plugin').getDevtoolsOptions(), this.options));
       await this.browser;
diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js
index 4aa998a4..8164adf0 100644
--- a/lib/helper/Protractor.js
+++ b/lib/helper/Protractor.js
@@ -1,6 +1,5 @@
 let By;
 let EC;
-let Runner;
 let Key;
 let Button;
 
@@ -21,11 +20,13 @@ const {
   convertColorToRGBA,
 } = require('../colorUtils');
 const ElementNotFound = require('./errors/ElementNotFound');
+const ConnectionRefused = require('./errors/ConnectionRefused');
 const Locator = require('../locator');
 const path = require('path');
 const recorder = require('../recorder');
 
 let withinStore = {};
+let Runner;
 
 /**
  * Protractor helper is based on [Protractor library](http://www.protractortest.org) and used for testing web applications.
@@ -48,6 +49,7 @@ let withinStore = {};
  * * `keepBrowserState` (optional, default: false)  - keep browser state between tests when `restart` set to false.
  * * `seleniumAddress` - Selenium address to connect (default: http://localhost:4444/wd/hub)
  * * `rootElement` - Root element of AngularJS application (default: body)
+ * * `getPageTimeout` (optional) sets default timeout for a page to be loaded. 10000 by default.
  * * `waitForTimeout`: (optional) sets default wait time in _ms_ for all `wait*` functions. 1000 by default.
  * * `scriptsTimeout`: (optional) timeout in milliseconds for each script run on the browser, 10000 by default.
  * * `windowSize`: (optional) default window size. Set to `maximize` or a dimension in the format `640x480`.
@@ -114,8 +116,15 @@ let withinStore = {};
  */
 class Protractor extends Helper {
   constructor(config) {
+    // process.env.SELENIUM_PROMISE_MANAGER = false; // eslint-disable-line
     super(config);
-    this.options = {
+
+    this.isRunning = false;
+    this._setConfig(config);
+  }
+
+  _validateConfig(config) {
+    const defaults = {
       browser: 'chrome',
       url: 'http://localhost',
       seleniumAddress: 'http://localhost:4444/wd/hub',
@@ -125,28 +134,33 @@ class Protractor extends Helper {
       scriptTimeout: 10000,
       waitForTimeout: 1000, // ms
       windowSize: null,
+      getPageTimeout: 10000,
       driver: 'hosted',
       capabilities: {},
       angular: true,
     };
 
-    this.isRunning = false;
+    config = Object.assign(defaults, config);
 
-    this.options = Object.assign(this.options, config);
-    if (!this.options.allScriptsTimeout) this.options.allScriptsTimeout = this.options.scriptsTimeout;
-    if (!this.options.scriptTimeout) this.options.scriptTimeout = this.options.scriptsTimeout;
-    if (this.options.proxy) this.options.capabilities.proxy = this.options.proxy;
-    if (this.options.browser) this.options.capabilities.browserName = this.options.browser;
-    this.options.waitForTimeout /= 1000; // convert to seconds
+    if (!config.allScriptsTimeout) config.allScriptsTimeout = config.scriptsTimeout;
+    if (!config.scriptTimeout) config.scriptTimeout = config.scriptsTimeout;
+    if (config.proxy) config.capabilities.proxy = config.proxy;
+    if (config.browser) config.capabilities.browserName = config.browser;
+    config.waitForTimeout /= 1000; // convert to seconds
+    return config;
   }
 
   async _init() {
+    process.on('unhandledRejection', (reason) => {
+      if (reason.message.indexOf('ECONNREFUSED') > 0) {
+        this.browser = null;
+      }
+    });
     Runner = requireg('protractor/built/runner').Runner;
     By = requireg('protractor').ProtractorBy;
     Key = requireg('protractor').Key;
     Button = requireg('protractor').Button;
 
-    this.context = this.options.rootElement;
     return Promise.resolve();
   }
 
@@ -185,33 +199,43 @@ class Protractor extends Helper {
   async _startBrowser() {
     const runner = new Runner(this.options);
     this.browser = runner.createBrowser();
+    try {
+      await this.browser.ready;
+    } catch (err) {
+      if (err.toString().indexOf('ECONNREFUSED')) {
+        throw new ConnectionRefused(err);
+      }
+      throw err;
+    }
+    if (this.options.angular) {
+      await this.amInsideAngularApp();
+    } else {
+      await this.amOutsideAngularApp();
+    }
     global.browser = this.browser;
     global.$ = this.browser.$;
     global.$$ = this.browser.$$;
     global.element = this.browser.element;
     global.by = global.By = new By();
     global.ExpectedConditions = EC = this.browser.ExpectedConditions;
-    const promisesList = [];
     if (this.options.windowSize === 'maximize') {
       await this.resizeWindow(this.options.windowSize);
     } else if (this.options.windowSize) {
       const size = this.options.windowSize.split('x');
       await this.resizeWindow(parseInt(size[0], 10), parseInt(size[1], 10));
     }
-    if (this.options.angular) {
-      await this.amInsideAngularApp();
-    } else {
-      await this.amOutsideAngularApp();
-    }
+    this.context = this.options.rootElement;
     this.isRunning = true;
+    return this.browser.ready;
   }
 
   async _before() {
-    if (this.options.restart && !this.options.manualStart) await this._startBrowser();
-    if (!this.isRunning && !this.options.manualStart) await this._startBrowser();
+    if (this.options.restart && !this.options.manualStart) return this._startBrowser();
+    if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
   }
 
   async _after() {
+    if (!this.browser) return;
     if (!this.isRunning) return;
     if (this.options.restart) {
       this.isRunning = false;
@@ -238,7 +262,7 @@ class Protractor extends Helper {
     return this.closeOtherTabs();
   }
 
-  async _failed(test) {
+  async _failed(test, err) {
     await this._withinEnd();
     if (this.options.disableScreenshots) return;
     let fileName = clearString(test.title);
@@ -291,7 +315,7 @@ class Protractor extends Helper {
   }
 
   async _withinEnd() {
-    if (!Object.keys(withinStore).length) return;
+    if (!isWithin()) return;
     if (withinStore.frame) {
       withinStore = {};
       return this.switchTo(null);
@@ -302,6 +326,33 @@ class Protractor extends Helper {
     this.context = this.options.rootElement;
   }
 
+  _session() {
+    const defaultSession = this.browser;
+    return {
+      start: async (opts) => {
+        opts = this._validateConfig(Object.assign(this.options, opts));
+        this.debugSection('New Browser', JSON.stringify(opts));
+        const runner = new Runner(opts);
+        const res = await this.browser.executeScript('return [window.outerWidth, window.outerHeight]');
+        const browser = runner.createBrowser(null, this.browser);
+        await browser.ready;
+        await browser.waitForAngularEnabled(this.insideAngular);
+        await browser.manage().window().setSize(parseInt(res[0], 10), parseInt(res[1], 10));
+        return browser.ready;
+      },
+      stop: async (browser) => {
+        return browser.close();
+      },
+      loadVars: async (browser) => {
+        if (isWithin()) throw new Error('Can\'t start session inside within block');
+        this.browser = browser;
+      },
+      restoreVars: async () => {
+        if (isWithin()) await this._withinEnd();
+        this.browser = defaultSession;
+      },
+    };
+  }
 
   /**
    * Switch to non-Angular mode,
@@ -879,7 +930,7 @@ class Protractor extends Helper {
   }
 
   /**
-   * {{> ../webapi/seeInCurrentUrl }}
+     * {{> ../webapi/seeInCurrentUrl }}
    */
   async seeInCurrentUrl(url) {
     return this.browser.getCurrentUrl().then(currentUrl => stringIncludes('url').assert(url, currentUrl));
@@ -911,7 +962,6 @@ class Protractor extends Helper {
    */
   async saveScreenshot(fileName, fullPage = false) {
     const outputFile = path.join(global.output_dir, fileName);
-    this.debug(`Screenshot has been saved to ${outputFile}`);
 
     const writeFile = (png, outputFile) => {
       const fs = require('fs');
@@ -922,16 +972,20 @@ class Protractor extends Helper {
     };
 
     if (!fullPage) {
+      this.debug(`Screenshot has been saved to ${outputFile}`);
       const png = await this.browser.takeScreenshot();
       return writeFile(png, outputFile);
     }
 
-    const { width, height } = await this.browser.executeScript(() => ({
+    let { width, height } = await this.browser.executeScript(() => ({ // eslint-disable-line
       height: document.body.scrollHeight,
       width: document.body.scrollWidth,
     }));
 
+    if (height < 100) height = 500;
+
     await this.browser.manage().window().setSize(width, height);
+    this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`);
     const png = await this.browser.takeScreenshot();
     return writeFile(png, outputFile);
   }
@@ -1648,3 +1702,7 @@ function isFrameLocator(locator) {
   if (locator.isFrame()) return locator.value;
   return false;
 }
+
+function isWithin() {
+  return Object.keys(withinStore).length !== 0;
+}
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index debf7e01..89a2e834 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -78,7 +78,15 @@ class Puppeteer extends Helper {
     puppeteer = requireg('puppeteer');
 
     // set defaults
-    this.options = {
+
+    this.isRunning = false;
+
+    // override defaults with config
+    this._setConfig(config);
+  }
+
+  _validateConfig(config) {
+    const defaults = {
       waitForAction: 100,
       waitForTimeout: 1000,
       fullPageScreenshots: true,
@@ -92,10 +100,11 @@ class Puppeteer extends Helper {
       defaultPopupAction: 'accept',
     };
 
-    this.isRunning = false;
+    return Object.assign(defaults, config);
+  }
 
-    // override defaults with config
-    Object.assign(this.options, config);
+  _setConfig(config) {
+    this.options = this._validateConfig(config);
     this.puppeteerOptions = Object.assign({ headless: !this.options.show }, this.options.chrome);
     popupStore.defaultAction = this.options.defaultPopupAction;
   }
@@ -1471,7 +1480,7 @@ class Puppeteer extends Helper {
       return currUrl.indexOf(urlPart) > -1;
     }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
       const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
-      if (/waiting/i.test(e.message)) { // waiting (for function) failed error message
+      if (/failed: timeout/i.test(e.message)) {
         throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
       } else {
         throw e;
@@ -1495,7 +1504,7 @@ class Puppeteer extends Helper {
       return currUrl.indexOf(urlPart) > -1;
     }, { timeout: waitTimeout }, urlPart).catch(async (e) => {
       const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data.
-      if (/waiting/i.test(e.message)) {
+      if (/failed: timeout/i.test(e.message)) {
         throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
       } else {
         throw e;
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index bff80da3..2ebba8dd 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -18,6 +18,8 @@ const {
   convertColorToRGBA,
 } = require('../colorUtils');
 const ElementNotFound = require('./errors/ElementNotFound');
+const ConnectionRefused = require('./errors/ConnectionRefused');
+
 const assert = require('assert');
 const path = require('path');
 
@@ -212,8 +214,16 @@ class WebDriverIO extends Helper {
   constructor(config) {
     super(config);
     webdriverio = requireg('webdriverio');
+    // set defaults
+    this.root = webRoot;
+
+    this.isRunning = false;
 
-    this.options = {
+    this._setConfig(config);
+  }
+
+  _validateConfig(config) {
+    const defaults = {
       smartWait: 0,
       waitForTimeout: 1000, // ms
       desiredCapabilities: {},
@@ -230,23 +240,14 @@ class WebDriverIO extends Helper {
       },
     };
 
-    this._validateConfig(config);
-  }
-
-  _validateConfig(config) {
-    // set defaults
-    this.root = webRoot;
-
-    this.isRunning = false;
-
     // override defaults with config
-    Object.assign(this.options, config);
+    config = Object.assign(defaults, config);
 
-    this.options.baseUrl = this.options.url || this.options.baseUrl;
-    this.options.desiredCapabilities.browserName = this.options.browser || this.options.desiredCapabilities.browserName;
-    this.options.waitForTimeout /= 1000; // convert to seconds
+    config.baseUrl = config.url || config.baseUrl;
+    config.desiredCapabilities.browserName = config.browser || config.desiredCapabilities.browserName;
+    config.waitForTimeout /= 1000; // convert to seconds
 
-    if (!this.options.desiredCapabilities.platformName && (!this.options.url || !this.options.browser)) {
+    if (!config.desiredCapabilities.platformName && (!config.url || !config.browser)) {
       throw new Error(`
         WebDriverIO requires at url and browser to be set.
         Check your codeceptjs config file to ensure these are set properly
@@ -260,6 +261,8 @@ class WebDriverIO extends Helper {
           }
       `);
     }
+
+    return config;
   }
 
   static _checkRequirements() {
@@ -295,7 +298,15 @@ class WebDriverIO extends Helper {
     } else {
       this.browser = webdriverio.remote(this.options).init();
     }
-    await this.browser;
+    try {
+      await this.browser;
+    } catch (err) {
+      if (err.toString().indexOf('ECONNREFUSED')) {
+        throw new ConnectionRefused(err);
+      }
+      throw err;
+    }
+
     this.isRunning = true;
     if (this.options.timeouts) {
       await this.defineTimeout(this.options.timeouts);
@@ -310,6 +321,10 @@ class WebDriverIO extends Helper {
     return this.browser;
   }
 
+  async _stopBrowser() {
+    if (this.browser && this.isRunning) await this.browser.end();
+  }
+
   async _before() {
     this.context = this.root;
     if (this.options.restart && !this.options.manualStart) return this._startBrowser();
@@ -341,11 +356,36 @@ class WebDriverIO extends Helper {
   }
 
   _finishTest() {
-    if (!this.options.restart && this.isRunning) return this.browser.end();
+    if (!this.options.restart) return this._startBrowser();
+  }
+
+  _session() {
+    const defaultSession = this.browser.requestHandler.sessionID;
+    return {
+      start: async (opts) => {
+        // 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));
+        const browser = webdriverio.remote(opts).init();
+        await browser;
+        return browser.requestHandler.sessionID;
+      },
+      stop: async (sessionId) => {
+        return this.browser.session('DELETE', sessionId);
+      },
+      loadVars: async (sessionId) => {
+        if (isWithin()) throw new Error('Can\'t start session inside within block');
+        this.browser.requestHandler.sessionID = sessionId;
+      },
+      restoreVars: async () => {
+        if (isWithin()) await this._withinEnd();
+        this.browser.requestHandler.sessionID = defaultSession;
+      },
+    };
   }
 
   async _failed(test) {
-    if (Object.keys(withinStore).length !== 0) await this._withinEnd();
+    if (isWithin()) await this._withinEnd();
     if (!this.options.disableScreenshots) {
       let fileName = clearString(test.title);
       if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`);
@@ -355,7 +395,7 @@ class WebDriverIO extends Helper {
       } else {
         fileName += '.failed.png';
       }
-      await this.saveScreenshot(fileName, this.options.fullPageScreenshots).catch((err) => {
+      return this.saveScreenshot(fileName, this.options.fullPageScreenshots).catch((err) => {
         if (err &&
             err.type &&
             err.type === 'RuntimeError' &&
@@ -1163,17 +1203,19 @@ class WebDriverIO extends Helper {
       return this.browser.saveScreenshot(outputFile);
     }
 
-    /* eslint-disable prefer-arrow-callback, comma-dangle */
-    const { width, height } = await this.browser.execute(function () {
+    /* eslint-disable prefer-arrow-callback, comma-dangle, prefer-const */
+    let { width, height } = await this.browser.execute(function () {
       return {
         height: document.body.scrollHeight,
         width: document.body.scrollWidth
       };
     }).then(res => res.value);
+
+    if (height < 100) height = 500; // errors for very small height
     /* eslint-enable */
 
     await this.browser.windowHandleSize({ width, height });
-    this.debug(`Screenshot has been saved to ${outputFile}`);
+    this.debug(`Screenshot has been saved to ${outputFile}, size: ${width}x${height}`);
     return this.browser.saveScreenshot(outputFile);
   }
 
@@ -2068,4 +2110,8 @@ function prepareLocateFn(context) {
   };
 }
 
+function isWithin() {
+  return Object.keys(withinStore).length !== 0;
+}
+
 module.exports = WebDriverIO;
diff --git a/lib/helper/errors/ConnectionRefused.js b/lib/helper/errors/ConnectionRefused.js
new file mode 100644
index 00000000..b3a1d3fa
--- /dev/null
+++ b/lib/helper/errors/ConnectionRefused.js
@@ -0,0 +1,10 @@
+function ConnectionRefused(err) {
+  this.message = "Can't connect to WebDriver.\n";
+  this.message += `${err}\n\n`;
+  this.message += 'Please make sure Selenium Server (ChromeDriver or PhantomJS) is running and accessible';
+  this.stack = err.stack;
+}
+
+ConnectionRefused.prototype = Object.create(Error.prototype);
+
+module.exports = ConnectionRefused;
diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js
index 661e7549..24761dac 100644
--- a/lib/interfaces/bdd.js
+++ b/lib/interfaces/bdd.js
@@ -5,10 +5,12 @@
 const Suite = require('mocha/lib/suite');
 const Test = require('mocha/lib/test');
 const scenario = require('../scenario');
+const ScenarioConfig = require('./scenarioConfig');
+const FeatureConfig = require('./featureConfig');
 const escapeRe = require('escape-string-regexp');
-
 const addDataContext = require('../data/context');
 
+
 /**
  * Codecept-style interface:
  *
@@ -52,14 +54,16 @@ module.exports = function (suite) {
       test.file = file;
       test.async = true;
       test.timeout(0);
-      test.inject = {};
+      if (!test.inject) {
+        test.inject = {};
+      }
 
       suite.addTest(scenario.test(test));
       if (opts.retries) test.retries(opts.retries);
       if (opts.timeout) test.timeout(opts.timeout);
       test.opts = opts;
 
-      return test;
+      return new ScenarioConfig(test);
     };
 
     // create dispatcher
@@ -100,7 +104,7 @@ module.exports = function (suite) {
       suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite));
       afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]);
 
-      return suite;
+      return new FeatureConfig(suite);
     };
 
     context.BeforeSuite = function (fn) {
diff --git a/lib/interfaces/featureConfig.js b/lib/interfaces/featureConfig.js
new file mode 100644
index 00000000..06ccf89d
--- /dev/null
+++ b/lib/interfaces/featureConfig.js
@@ -0,0 +1,26 @@
+class FeatureConfig {
+  constructor(suite) {
+    this.suite = suite;
+  }
+
+  /**
+   * Retry this suite for x times
+   *
+   * @param {*} retries
+   */
+  retry(retries) {
+    this.suite.retries(retries);
+    return this;
+  }
+
+  /**
+   * Set timeout for this suite
+   * @param {*} timeout
+   */
+  timeout(timeout) {
+    this.suite.timeout(timeout);
+    return this;
+  }
+}
+
+module.exports = FeatureConfig;
diff --git a/lib/interfaces/scenarioConfig.js b/lib/interfaces/scenarioConfig.js
new file mode 100644
index 00000000..df91de5b
--- /dev/null
+++ b/lib/interfaces/scenarioConfig.js
@@ -0,0 +1,58 @@
+class ScenarioConfig {
+  constructor(test) {
+    this.test = test;
+  }
+
+  /**
+   * Declares that test throws error.
+   * Can pass an Error object or regex matching expected message.
+   *
+   * @param {*} err
+   */
+  throws(err) {
+    this.test.throws = err;
+    return this;
+  }
+
+  /**
+   * Declares that test should fail.
+   * If test passes - throws an error.
+   * Can pass an Error object or regex matching expected message.
+   *
+   * @param {*} err
+   */
+  fails() {
+    this.test.throws = new Error();
+    return this;
+  }
+
+  /**
+   * Retry this test for x times
+   *
+   * @param {*} retries
+   */
+  retry(retries) {
+    this.test.retries(retries);
+    return this;
+  }
+
+  /**
+   * Set timeout for this test
+   * @param {*} timeout
+   */
+  timeout(timeout) {
+    this.test.timeout(timeout);
+    return this;
+  }
+
+  /**
+   * Pass in additional objects to inject into test
+   * @param {*} obj
+   */
+  inject(obj) {
+    this.test.inject = obj;
+    return this;
+  }
+}
+
+module.exports = ScenarioConfig;
diff --git a/lib/listener/steps.js b/lib/listener/steps.js
index 5fb9a8ba..a90bc3a5 100644
--- a/lib/listener/steps.js
+++ b/lib/listener/steps.js
@@ -51,7 +51,7 @@ module.exports = function () {
     currentTest.state = 'passed';
   });
 
-  event.dispatcher.on(event.step.before, (step) => {
+  event.dispatcher.on(event.step.started, (step) => {
     if (currentHook && Array.isArray(currentHook.steps)) {
       return currentHook.steps.push(step);
     }
diff --git a/lib/recorder.js b/lib/recorder.js
index 9c5fa3c8..329cc8a5 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -1,5 +1,6 @@
 const promiseRetry = require('promise-retry');
 const log = require('./output').log;
+const { isAsyncFunction } = require('./utils');
 
 let promise;
 let running = false;
@@ -93,11 +94,12 @@ module.exports = {
       log(`${currentQueue()}Finalize <${name}> session`);
       this.running = false;
       sessionId = null;
+      this.catch(errFn);
       promise = promise.then(() => oldPromises.pop());
     },
 
-    catch(errFn) {
-      promise = promise.catch(errFn);
+    catch(fn) {
+      promise = promise.catch(fn);
     },
 
   },
@@ -119,7 +121,7 @@ module.exports = {
       return;
     }
     tasks.push(taskName);
-    log(`${currentQueue()}Queued | ${taskName}`);
+    // log(`${currentQueue()}Queued | ${taskName}`);
 
     return promise = Promise.resolve(promise).then((res) => {
       const retryOpts = this.retries.slice(-1).pop();
diff --git a/lib/scenario.js b/lib/scenario.js
index 663365df..bcf3a65e 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -1,6 +1,7 @@
 const event = require('./event');
 const container = require('./container');
 const recorder = require('./recorder');
+const assertThrown = require('./assert/throws');
 const { getParamNames, isGenerator, isAsyncFunction } = require('./utils');
 
 const resumeTest = function (res, done, error, returnStatement) {
@@ -55,14 +56,29 @@ module.exports.test = (test) => {
     recorder.errHandler((err) => {
       recorder.session.start('teardown');
       recorder.cleanAsyncErr();
+      if (test.throws) { // check that test should actually fail
+        try {
+          assertThrown(err, test.throws);
+          event.emit(event.test.passed, test);
+          event.emit(event.test.finished, test);
+          recorder.add(() => done());
+          return;
+        } catch (newErr) {
+          err = newErr;
+        }
+      }
       event.emit(event.test.failed, test, err);
+      event.emit(event.test.finished, test);
       recorder.add(() => done(err));
     });
 
     if (isAsyncFunction(testFn)) {
       event.emit(event.test.started, test);
       testFn.apply(test, getInjectedArguments(testFn, test)).then((res) => {
-        recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
+        recorder.add('fire test.passed and test.finished', () => {
+          event.emit(event.test.passed, test);
+          event.emit(event.test.finished, test);
+        });
         recorder.add('finish test', () => done());
         recorder.catch();
       }).catch((e) => {
@@ -72,6 +88,7 @@ module.exports.test = (test) => {
           recorder.session.start('teardown');
           recorder.cleanAsyncErr();
           event.emit(event.test.failed, test, err);
+          event.emit(event.test.finished, test);
           recorder.add(() => done(err));
         });
       });
@@ -90,7 +107,10 @@ module.exports.test = (test) => {
         }
         recorder.catch(); // catching possible errors in promises
         resumeTest(res, () => {
-          recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
+          recorder.add('fire test.passed', () => {
+            event.emit(event.test.passed, test);
+            event.emit(event.test.finished, test);
+          });
           recorder.add('finish generator with no error', () => done()); // finish him
         });
       }
@@ -98,7 +118,10 @@ module.exports.test = (test) => {
       recorder.throw(err);
     } finally {
       if (!isGenerator(testFn)) {
-        recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
+        recorder.add('fire test.passed', () => {
+          event.emit(event.test.passed, test);
+          event.emit(event.test.finished, test);
+        });
         recorder.add('finish test', () => done());
         recorder.catch();
       }
diff --git a/lib/session.js b/lib/session.js
new file mode 100644
index 00000000..48550b4b
--- /dev/null
+++ b/lib/session.js
@@ -0,0 +1,125 @@
+const recorder = require('./recorder');
+const container = require('./container');
+const event = require('./event');
+const output = require('./output');
+const { getParamNames, isGenerator, isAsyncFunction } = require('./utils');
+const resumeTest = require('./scenario').resumeTest;
+
+const sessionColors = [
+  'cyan',
+  'blue',
+  'red',
+  'magenta',
+  'yellow',
+];
+
+const savedSessions = {};
+
+function session(sessionName, config, fn) {
+  if (typeof config === 'function') {
+    if (typeof fn === 'function') {
+      config = config();
+    } else {
+      // no config but with function
+      fn = config;
+      config = {};
+    }
+  }
+  // session helpers won't use beforeSuite and afterSuite hooks...
+  // restart: false options are not allowed as well
+  // but those helpers should be already started so inside listener/helpers.js the `_init` method should already be called
+
+  const helpers = container.helpers();
+
+  if (!savedSessions[sessionName]) {
+    for (const helper in helpers) {
+      if (!helpers[helper]._session) continue;
+      savedSessions[sessionName] = Object.assign({
+        start: () => null,
+        stop: () => null,
+        loadVars: () => null,
+        restoreVars: () => null,
+      }, helpers[helper]._session());
+      break;
+    }
+
+
+    const closeBrowsers = () => {
+      const session = savedSessions[sessionName];
+      if (!session) return;
+      session.stop(session.vars);
+      delete savedSessions[sessionName];
+    };
+
+    event.dispatcher.once(event.test.finished, () => closeBrowsers());
+
+    if (!savedSessions[sessionName]) {
+      throw new Error('Configured helpers do not support starting sessions. Please use a helper with session support.');
+    }
+
+    recorder.add('save vars', async () => {
+      savedSessions[sessionName].vars = await savedSessions[sessionName].start(config);
+    });
+  }
+
+  // pick random color
+  const color = sessionColors[Object.keys(savedSessions).indexOf(sessionName) % sessionColors.length];
+
+  const addContextToStep = (step) => {
+    step.actor = `${output.colors[color](sessionName)}: I`;
+  };
+
+  if (!fn) return; // no current session steps
+
+
+  recorder.add('register session wrapper', async () => {
+    const session = savedSessions[sessionName];
+    recorder.session.start(`session:${sessionName}`);
+    event.dispatcher.on(event.step.after, addContextToStep);
+    recorder.add('switch to browser', () => {
+      const session = savedSessions[sessionName];
+      session.loadVars(session.vars);
+    });
+
+    const finalize = () => {
+      recorder.add('Finalize session', () => {
+        output.stepShift = 0;
+        event.dispatcher.removeListener(event.step.after, addContextToStep);
+        session.restoreVars();
+        recorder.session.restore(`session:${sessionName}`);
+      });
+    };
+
+    if (isAsyncFunction(fn)) {
+      return fn.apply(null).then(() => {
+        finalize();
+        return recorder.promise();
+      }).catch((e) => {
+        output.stepShift = 0;
+        session.restoreVars();
+        event.dispatcher.removeListener(event.step.after, addContextToStep);
+        recorder.throw(e);
+        return recorder.promise();
+      });
+    }
+
+    try {
+      const res = fn.apply(null);
+    } catch (err) {
+      recorder.throw(err);
+    } finally {
+      finalize();
+      recorder.catch((e) => {
+        session.restoreVars();
+        output.stepShift = 0;
+        event.dispatcher.removeListener(event.step.after, addContextToStep);
+        throw e;
+      });
+    }
+    finalize();
+    return recorder.promise();
+  });
+}
+
+
+module.exports = session;
diff --git a/lib/utils.js b/lib/utils.js
index 27b78a50..7665b914 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -172,6 +172,25 @@ module.exports.xpathLocator = {
 };
 
 module.exports.test = {
+
+  grepLines(array, startString, endString) {
+    let startIndex = 0;
+    let endIndex;
+    array.every((elem, index) => {
+      if (elem === startString) {
+        startIndex = index;
+        return true;
+      }
+      if (elem === endString) {
+        endIndex = index;
+        return false;
+      }
+      return true;
+    });
+    return array.slice(startIndex + 1, endIndex);
+  },
+
+
   submittedData(dataFile) {
     return function (key) {
       if (!fs.existsSync(dataFile)) {
diff --git a/package.json b/package.json
index 5330ea5e..79d9c230 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
   },
   "devDependencies": {
     "@types/inquirer": "^0.0.35",
-    "@types/node": "^8.9.3",
+    "@types/node": "^8.10.12",
     "chai": "^3.4.1",
     "chai-as-promised": "^5.2.0",
     "co-mocha": "^1.2",
@@ -67,7 +67,7 @@
     "selenium-webdriver": "^2.53.1",
     "sinon": "^1.17.2",
     "sinon-chai": "^2.14.0",
-    "typescript": "^2.7.1",
+    "typescript": "^2.8.3",
     "unirest": "^0.5.1",
     "webdriverio": "^4.10.2"
   },


Test patch:
diff --git a/test/acceptance/codecept.Protractor.js b/test/acceptance/codecept.Protractor.js
index 154b6775..908a870b 100644
--- a/test/acceptance/codecept.Protractor.js
+++ b/test/acceptance/codecept.Protractor.js
@@ -8,6 +8,7 @@ module.exports.config = {
     Protractor: {
       url: TestHelper.siteUrl(),
       browser: 'chrome',
+      // restart: true,
       angular: false,
       host: TestHelper.seleniumHost(),
       port: TestHelper.seleniumPort(),
diff --git a/test/acceptance/codecept.WebDriverIO.js b/test/acceptance/codecept.WebDriverIO.js
index bc41adf1..0f74a02c 100644
--- a/test/acceptance/codecept.WebDriverIO.js
+++ b/test/acceptance/codecept.WebDriverIO.js
@@ -10,11 +10,12 @@ module.exports.config = {
       browser: 'chrome',
       host: TestHelper.seleniumHost(),
       port: TestHelper.seleniumPort(),
-      desiredCapabilities: {
-        chromeOptions: {
-          args: ['--headless', '--disable-gpu', '--window-size=1280,1024'],
-        },
-      },
+      // disableScreenshots: true,
+      // desiredCapabilities: {
+      //   chromeOptions: {
+      //     args: ['--headless', '--disable-gpu', '--window-size=1280,1024'],
+      //   },
+      // },
     },
 
   },
diff --git a/test/acceptance/session_test.js b/test/acceptance/session_test.js
new file mode 100644
index 00000000..1b5ef127
--- /dev/null
+++ b/test/acceptance/session_test.js
@@ -0,0 +1,128 @@
+const chai = require('chai');
+
+const expect = chai.expect;
+
+Feature('Session');
+
+Scenario('simple session @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/info');
+  session('john', () => {
+    I.amOnPage('https://github.com');
+    I.dontSeeInCurrentUrl('/info');
+    I.see('GitHub');
+  });
+  I.dontSee('GitHub');
+  I.seeInCurrentUrl('/info');
+});
+
+Scenario('Different cookies for different sessions @WebDriverIO @Protractor', async (I) => {
+  const cookiePage = 'https://www.microsoft.com/en-au/';
+  const cookieName = 'MUID';
+  const cookies = {};
+
+  I.amOnPage(cookiePage);
+  session('john', () => {
+    I.amOnPage(cookiePage);
+  });
+  session('mary', () => {
+    I.amOnPage(cookiePage);
+  });
+
+  cookies.default = (await I.grabCookie(cookieName)).value;
+  I.say(`${cookieName}: ${cookies.default}`);
+  session('john', async () => {
+    cookies.john = (await I.grabCookie(cookieName)).value;
+    I.say(`${cookieName}: ${cookies.john}`);
+  });
+  session('mary', async () => {
+    cookies.mary = (await I.grabCookie(cookieName)).value;
+    I.say(`${cookieName}: ${cookies.mary}`);
+  });
+  await I.seeInCurrentUrl('en-au');
+  expect(cookies.default).to.be.ok;
+  expect(cookies.john).to.be.ok;
+  expect(cookies.mary).to.be.ok;
+  expect(cookies.default).to.not.equal(cookies.john);
+  expect(cookies.default).to.not.equal(cookies.mary);
+  expect(cookies.john).to.not.equal(cookies.mary);
+});
+
+Scenario('should throw exception and close correctly @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/form/bug1467#session1');
+  I.checkOption('Yes');
+  session('john', () => {
+    I.amOnPage('/form/bug1467#session2');
+    I.checkOption('No1');
+    I.seeCheckboxIsChecked({ css: 'input[value=No]' });
+  });
+  I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
+}).fails();
+
+Scenario('async/await @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/form/bug1467#session1');
+  I.checkOption('Yes');
+  session('john', async () => {
+    I.amOnPage('/form/bug1467#session2');
+    I.checkOption('No');
+    I.seeCheckboxIsChecked({ css: 'input[value=No]' });
+  });
+  I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
+});
+
+Scenario('exception on async/await @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/form/bug1467#session1');
+  I.checkOption('Yes');
+  session('john', async () => {
+    I.amOnPage('/form/bug1467#session2');
+    I.checkOption('No');
+    I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
+  });
+  I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
+}).throws(/to be checked/);
+
+Scenario('should work with within @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/form/bug1467');
+  session('john', () => {
+    I.amOnPage('/form/bug1467');
+    within({ css: '[name=form1]' }, () => {
+      I.checkOption('Yes');
+      I.seeCheckboxIsChecked({ css: 'input[name=first_test_radio]' });
+    });
+  });
+  within({ css: '[name=form2]' }, () => {
+    I.checkOption('Yes');
+    I.seeCheckboxIsChecked({ css: 'input[name=first_test_radio]' });
+  });
+  I.seeCheckboxIsChecked({ css: 'form[name=form2] input[name=first_test_radio]' });
+  I.dontSeeCheckboxIsChecked({ css: 'form[name=form1] input[name=first_test_radio]' });
+  session('john', () => {
+    I.seeCheckboxIsChecked({ css: 'form[name=form1] input[name=first_test_radio]' });
+    I.dontSeeCheckboxIsChecked({ css: 'form[name=form2] input[name=first_test_radio]' });
+  });
+});
+
+Scenario('should use different base URL @WebDriverIO @Protractor', (I) => {
+  I.amOnPage('/');
+  I.see('Welcome to test app');
+  session('john', { url: 'https://github.com' }, () => {
+    I.amOnPage('/');
+    I.dontSee('Welcome to test app');
+    I.see('GitHub');
+  });
+  I.see('Welcome to test app');
+});
+
+Scenario('should start firefox @WebDriverIO', async (I) => {
+  I.amOnPage('/form/bug1467#session1');
+  I.checkOption('Yes');
+  session('john', { browser: 'firefox' }, async () => {
+    I.amOnPage('/form/bug1467#session2');
+    I.checkOption('No');
+    I.seeCheckboxIsChecked({ css: 'input[value=No]' });
+    const isChrome = await I.executeScript(() => !!window.chrome);
+    expect(isChrome).not.to.be.ok;
+  });
+  I.seeCheckboxIsChecked({ css: 'input[value=Yes]' });
+  const isChrome = await I.executeScript(() => !!window.chrome);
+  expect(isChrome).to.be.ok;
+});
diff --git a/test/data/sandbox/base_test_session.js b/test/data/sandbox/base_test_session.js
new file mode 100644
index 00000000..ff2ccf18
--- /dev/null
+++ b/test/data/sandbox/base_test_session.js
@@ -0,0 +1,40 @@
+Feature('Session');
+
+Scenario('basic session @1', (I) => {
+  I.do('writing');
+  session('davert', () => {
+    I.do('reading');
+  });
+  I.do('playing');
+  session('john', () => {
+    I.do('crying');
+  });
+  session('davert', () => {
+    I.do('smiling');
+  });
+  I.do('laughing');
+  session('mike', () => {
+    I.do('spying');
+  });
+  session('john', () => {
+    I.do('lying');
+  });
+  I.do('waving');
+});
+
+Scenario('session defined not used @2', (I) => {
+  session('davert');
+  I.do('writing');
+  I.do('playing');
+  session('john', () => {
+    I.do('crying');
+  });
+  session('davert', () => {
+    I.do('smiling');
+  });
+  I.do('laughing');
+  session('davert', () => {
+    I.do('singing');
+  });
+  I.do('waving');
+});
diff --git a/test/data/sandbox/codecept.multiple.json b/test/data/sandbox/codecept.multiple.json
index 5adf5841..c1f9e1c3 100644
--- a/test/data/sandbox/codecept.multiple.json
+++ b/test/data/sandbox/codecept.multiple.json
@@ -9,9 +9,13 @@
       "windowSize": "maximize"
     }
   },
+
   "multiple": {
     "default": {
-      "browsers": ["chrome", { "browser": "firefox"}]
+      "browsers": [
+        "chrome", 
+        { "browser": "firefox"}
+      ]
     },
     "mobile": {
       "browsers": [
@@ -19,17 +23,21 @@
         {"browser": "safari", "windowSize": "maximize"},
         {"browser": "chrome", "windowSize": "maximize"},
         {"browser": "safari", "windowSize": "1200x840"}
-        ]
+      ]
     },
     "grep": {
       "grep": "@grep",
-      "browsers": ["chrome",
-        { "browser": "firefox", "windowSize": "1200x840"}]
+      "browsers": [
+        "chrome",
+        { "browser": "firefox", "windowSize": "1200x840"}
+      ]
     },
     "test": {
       "tests": "./*_test_override.multiple.js",
-      "browsers": ["chrome",
-        { "browser": "firefox", "windowSize": "1200x840"}]
+      "browsers": [
+        "chrome",
+        { "browser": "firefox", "windowSize": "1200x840"}
+      ]
     }
   },
   "include": {},
diff --git a/test/data/sandbox/codecept.session.json b/test/data/sandbox/codecept.session.json
new file mode 100644
index 00000000..632f282e
--- /dev/null
+++ b/test/data/sandbox/codecept.session.json
@@ -0,0 +1,14 @@
+{
+  "tests": "./*test_session.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "Session": {
+      "require": "./session_helper"
+    }
+  },
+  "include": {},
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
diff --git a/test/data/sandbox/flaky_test.flaky.js b/test/data/sandbox/flaky_test.flaky.js
index e1b9e8be..03a8ec38 100644
--- a/test/data/sandbox/flaky_test.flaky.js
+++ b/test/data/sandbox/flaky_test.flaky.js
@@ -2,14 +2,15 @@ const assert = require('assert');
 
 let tries = 0;
 let tries2 = 0;
+let tries3 = 0;
 
-Feature('Flaky', { retries: 4 });
+Feature('Flaky').retry(4);
 
-Scenario('Not so flaky test', { retries: 2 }, () => {
+Scenario('Not so flaky test', () => {
   tries++;
   assert.equal(tries, 2);
   console.log(`[T1] Retries: ${tries}`);
-});
+}).retry(2);
 
 Scenario('Really flaky test', () => {
   tries2++;
@@ -17,3 +18,9 @@ Scenario('Really flaky test', () => {
   console.log(`[T2] Retries: ${tries2}`);
 });
 
+Scenario('Old style flaky', { retries: 1 }, () => {
+  tries3++;
+  assert.equal(tries3, 1);
+  console.log(`[T3] Retries: ${tries3}`);
+});
+
diff --git a/test/data/sandbox/session_helper.js b/test/data/sandbox/session_helper.js
new file mode 100644
index 00000000..46af0be2
--- /dev/null
+++ b/test/data/sandbox/session_helper.js
@@ -0,0 +1,40 @@
+
+const Helper = require('../../../lib/helper');
+const output = require('../../../lib/output');
+
+let uniqueSessions = 0;
+
+class Session extends Helper {
+  _init() {
+    this.sessionId = uniqueSessions;
+  }
+
+  _before() {
+    uniqueSessions = 0;
+  }
+
+  _session() {
+    const defaultSession = this.sessionId;
+    return {
+      start: () => ++uniqueSessions,
+      stop: () => {},
+      loadVars: sessionSaved => this.sessionId = sessionSaved,
+      restoreVars: () => this.sessionId = defaultSession,
+    };
+  }
+
+  do(action) {
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        resolve('res');
+      }, 100);
+    });
+    // .then(() => output.step(`session:${this.sessionId}.${action}`));
+  }
+
+  errorStep() {
+    throw new Error('ups, error');
+  }
+}
+
+module.exports = Session;
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index ddd167c9..26476117 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -21,6 +21,15 @@ services:
       - ..:/codecept
       - node_modules:/codecept/node_modules
 
+  test-runner:
+    build: ..
+    entrypoint: /codecept/node_modules/.bin/mocha
+    command: test/runner
+    working_dir: /codecept
+    volumes:
+      - ..:/codecept
+      - node_modules:/codecept/node_modules
+
   test-acceptance.webdriverio:
     build: ..
     env_file: .env
diff --git a/test/helper/Protractor_test.js b/test/helper/Protractor_test.js
index 54f07bf6..9100cb15 100644
--- a/test/helper/Protractor_test.js
+++ b/test/helper/Protractor_test.js
@@ -9,7 +9,7 @@ const formContents = require('../../lib/utils').test.submittedData(path.join(__d
 const fileExists = require('../../lib/utils').fileExists;
 
 const web_app_url = TestHelper.siteUrl();
-const siteUrl = 'http://davertmik.github.io/angular-demo-app';
+const siteUrl = TestHelper.angularSiteUrl();
 let I;
 let browser;
 
@@ -34,6 +34,8 @@ describe('Protractor', function () {
       seleniumAddress: TestHelper.seleniumAddress(),
       angular: true,
       waitForTimeout: 5000,
+      getPageTimeout: 120000,
+      allScriptsTimeout: 30000,
       capabilities: {
         chromeOptions: {
           args: ['--headless', '--disable-gpu', '--window-size=1280,1024'],
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 33ba4e9d..b5622ebb 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -25,7 +25,7 @@ describe('Puppeteer', function () {
       url: siteUrl,
       windowSize: '500x700',
       show: false,
-      waitForTimeout: 2000,
+      waitForTimeout: 5000,
       waitForAction: 500,
       chrome: {
         args: ['--no-sandbox', '--disable-setuid-sandbox'],
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 89db6ebb..43719172 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -17,6 +17,10 @@ describe('CodeceptJS Interface', () => {
     exec(config_run_config('codecept.flaky.json'), (err, stdout, stderr) => {
       stdout.should.include('Flaky'); // feature
       stdout.should.include('Not so flaky test'); // test name
+      stdout.should.include('Old style flaky'); // test name
+      stdout.should.include('[T1] Retries: 2'); // test name
+      stdout.should.include('[T2] Retries: 4'); // test name
+      stdout.should.include('[T3] Retries: 1'); // test name
       assert(!err);
       done();
     });
@@ -69,30 +73,6 @@ describe('CodeceptJS Interface', () => {
 
   it('should execute expected promise chain', (done) => {
     exec(`${codecept_run} --verbose`, (err, stdout, stderr) => {
-      const queue1 = stdout.match(/\[1\] .+/g);
-      queue1.should.eql([
-        '[1] Starting recording promises',
-        '[1] Queued | hook FileSystem._beforeSuite()',
-        '[1] Queued | hook FileSystem._before()',
-        '[1] Queued | hook FileSystem._beforeStep()',
-        '[1] Queued | amInPath: "."',
-        '[1] Queued | hook FileSystem._afterStep()',
-        '[1] Queued | step passed',
-        '[1] Queued | return result',
-        '[1] Queued | say hello world',
-        '[1] Queued | hook FileSystem._beforeStep()',
-        '[1] Queued | seeFile: "codecept.json"',
-        '[1] Queued | hook FileSystem._afterStep()',
-        '[1] Queued | step passed',
-        '[1] Queued | return result',
-        '[1] Queued | fire test.passed',
-        '[1] Queued | finish test',
-        '[1] Queued | hook FileSystem._passed()',
-        '[1] Queued | hook FileSystem._after()',
-        '[1] Queued | hook FileSystem._afterSuite()',
-        '[1] Queued | hook FileSystem._finishTest()',
-      ]);
-
       const lines = stdout.match(/\S.+/g);
 
       // before hooks
diff --git a/test/runner/run_multiple_test.js b/test/runner/run_multiple_test.js
index 65704b99..d4035057 100644
--- a/test/runner/run_multiple_test.js
+++ b/test/runner/run_multiple_test.js
@@ -61,7 +61,6 @@ describe('CodeceptJS Multiple Runner', function () {
       stdout.should.include('[4.mobile:safari] print browser ');
       stdout.should.include('[5.default:chrome] print browser ');
       stdout.should.include('[6.default:firefox] print browser ');
-
       assert(!err);
       done();
     });
diff --git a/test/runner/session_test.js b/test/runner/session_test.js
new file mode 100644
index 00000000..b5481aca
--- /dev/null
+++ b/test/runner/session_test.js
@@ -0,0 +1,70 @@
+const path = require('path');
+const exec = require('child_process').exec;
+const { grepLines } = require('../../lib/utils').test;
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox');
+const codecept_run = `${runner} run --config ${codecept_dir}/codecept.session.json `;
+
+describe('CodeceptJS session', function () {
+  this.timeout(40000);
+
+  before(() => {
+    global.codecept_dir = path.join(__dirname, '/../data/sandbox');
+  });
+
+  it('should run with 3 sessions', (done) => {
+    exec(`${codecept_run} --steps --grep "@1"`, (err, stdout, stderr) => {
+      const lines = stdout.match(/\S.+/g);
+
+      const list = grepLines(lines, 'basic session @1');
+      list.pop();
+      testStatus = list.pop();
+      testStatus.should.include('OK');
+      list.should.eql([
+        '• I do "writing"',
+        '• davert: I do "reading"',
+        '• I do "playing"',
+        '• john: I do "crying"',
+        '• davert: I do "smiling"',
+        '• I do "laughing"',
+        '• mike: I do "spying"',
+        '• john: I do "lying"',
+        '• I do "waving"',
+      ], 'check steps execution order');
+      done();
+    });
+  });
+
+  it('should run session defined before executing', (done) => {
+    exec(`${codecept_run} --steps --grep "@2"`, (err, stdout, stderr) => {
+      const lines = stdout.match(/\S.+/g);
+
+      const list = grepLines(lines, 'session defined not used @2');
+      list.pop();
+      testStatus = list.pop();
+      testStatus.should.include('OK');
+
+      list.should.eql([
+        '• I do "writing"',
+        '• I do "playing"',
+        '• john: I do "crying"',
+        '• davert: I do "smiling"',
+        '• I do "laughing"',
+        '• davert: I do "singing"',
+        '• I do "waving"',
+      ], 'check steps execution order');
+      done();
+    });
+  });
+
+  it('should run all session tests', (done) => {
+    exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
+      const lines = stdout.match(/\S.+/g);
+      const testStatus = lines.pop();
+      testStatus.should.include('passed');
+      testStatus.should.not.include(' 1 ', 'more than 1 test expected');
+      done();
+    });
+  });
+});
diff --git a/test/runner/within_test.js b/test/runner/within_test.js
index 42fef921..977b9c72 100644
--- a/test/runner/within_test.js
+++ b/test/runner/within_test.js
@@ -1,26 +1,11 @@
 const path = require('path');
 const exec = require('child_process').exec;
+const { grepLines } = require('../../lib/utils').test;
 
 const runner = path.join(__dirname, '/../../bin/codecept.js');
 const codecept_dir = path.join(__dirname, '/../data/sandbox');
 const codecept_run = `${runner} run --config ${codecept_dir}/codecept.within.json `;
 
-const getLines = function (array, startString, endString) {
-  let startIndex;
-  let endIndex;
-  array.every((elem, index) => {
-    if (elem === startString) {
-      startIndex = index;
-      return true;
-    }
-    if (elem === endString) {
-      endIndex = index;
-      return false;
-    }
-    return true;
-  });
-  return array.slice(startIndex + 1, endIndex);
-};
 let testStatus;
 
 describe('CodeceptJS within', function () {
@@ -34,7 +19,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withoutGeneratorList = getLines(lines, 'Check within without generator', 'Check within with generator. Yield is first in order');
+      const withoutGeneratorList = grepLines(lines, 'Check within without generator', 'Check within with generator. Yield is first in order');
       testStatus = withoutGeneratorList.pop();
       testStatus.should.include('OK');
       withoutGeneratorList.should.eql([
@@ -54,7 +39,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withGeneratorList = getLines(lines, 'Check within with generator. Yield is first in order', 'Check within with generator. Yield is second in order');
+      const withGeneratorList = grepLines(lines, 'Check within with generator. Yield is first in order', 'Check within with generator. Yield is second in order');
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
@@ -79,7 +64,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withGeneratorListOtherOrder = getLines(lines, 'Check within with generator. Yield is second in order', 'Check within with generator. Should complete test steps after within');
+      const withGeneratorListOtherOrder = grepLines(lines, 'Check within with generator. Yield is second in order', 'Check within with generator. Should complete test steps after within');
       testStatus = withGeneratorListOtherOrder.pop();
       testStatus.should.include('OK');
       withGeneratorListOtherOrder.should.eql([
@@ -104,7 +89,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withGeneratorList = getLines(lines, 'Check within with async/await. Await is first in order', 'Check within with async/await. Await is second in order');
+      const withGeneratorList = grepLines(lines, 'Check within with async/await. Await is first in order', 'Check within with async/await. Await is second in order');
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
@@ -129,7 +114,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withGeneratorList = getLines(lines, 'Check within with async/await. Await is second in order', '-- FAILURES:');
+      const withGeneratorList = grepLines(lines, 'Check within with async/await. Await is second in order', '-- FAILURES:');
       testStatus = withGeneratorList.pop();
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
@@ -155,7 +140,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const withGeneratorListWithContinued = getLines(lines, 'Check within with generator. Should complete test steps after within', 'Check within with generator. Should stop test execution after fail in within');
+      const withGeneratorListWithContinued = grepLines(lines, 'Check within with generator. Should complete test steps after within', 'Check within with generator. Should stop test execution after fail in within');
       testStatus = withGeneratorListWithContinued.pop();
       testStatus.should.include('OK');
       withGeneratorListWithContinued.should.eql([
@@ -179,7 +164,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const errorInWithinList = getLines(lines, 'Check within with generator. Should stop test execution after fail in within', 'Check within with generator. Should stop test execution after fail in main block');
+      const errorInWithinList = grepLines(lines, 'Check within with generator. Should stop test execution after fail in within', 'Check within with generator. Should stop test execution after fail in main block');
       testStatus = errorInWithinList.pop();
       testStatus.should.include('FAILED');
       errorInWithinList.should.eql([
@@ -199,7 +184,7 @@ describe('CodeceptJS within', function () {
     exec(`${codecept_run} --steps`, (err, stdout, stderr) => {
       const lines = stdout.match(/\S.+/g);
 
-      const errorInTestList = getLines(lines, 'Check within with generator. Should stop test execution after fail in main block', 'Check within with async/await. Await is first in order');
+      const errorInTestList = grepLines(lines, 'Check within with generator. Should stop test execution after fail in main block', 'Check within with async/await. Await is first in order');
       testStatus = errorInTestList.pop();
       testStatus.should.include('FAILED');
       errorInTestList.should.eql([
diff --git a/test/support/TestHelper.js b/test/support/TestHelper.js
index 2c6a9ce3..a1028180 100644
--- a/test/support/TestHelper.js
+++ b/test/support/TestHelper.js
@@ -3,6 +3,10 @@ class TestHelper {
     return (process.env.SITE_URL || 'http://localhost:8000');
   }
 
+  static angularSiteUrl() {
+    return 'http://davertmik.github.io/angular-demo-app';
+  }
+
   static seleniumAddress() {
     return `http://${this.seleniumHost()}:${this.seleniumPort()}/wd/hub`;
   }