Repository: Codeception/CodeceptJS
Pull request #1049 (2018-05-05 08:49:22)

Multiple Sessions
```js
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('', (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;
});
```

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 5
           

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/runner/within_test.js  -1
+ lib/scenario.js  -2
         
```


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


```diff

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/runner/within_test.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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 1
           

Complexity increasing per file
==============================
- lib/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Puppeteer.js  1
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  3
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  2
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        
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/interfaces/bdd.js  1
         

Clones added
============
- lib/helper/Protractor.js  3
- lib/helper/WebDriverIO.js  2
         

Clones removed
==============
+ test/acceptance/codecept.Protractor.js  -1
+ test/runner/within_test.js  -1
+ test/runner/interface_test.js  -1
+ test/acceptance/codecept.WebDriverIO.js  -1
+ lib/scenario.js  -2
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=1616095&bid=7238705)
        

Patch:
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..02b57b14 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:
 
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/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/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 c104ea2d..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() {
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.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/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/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`;
   }