Repository: Codeception/CodeceptJS
Pull request #606 (2017-07-17 18:06:40)

New Webdriverio methods
new Methods:
* grabCssPropertyFrom
* seeTitleEquals
* seeTextEquals
* seeCssPropertiesOnElements
* seeAttributesOnElements
* grabNumberOfVisibleElements
* waitInUrl
* waitUrlEquals
* waitForValue
* waitNumberOfVisibleElements
* switchToNextTab
* switchToPreviousTab
* closeCurrentTab
* openNewTab
* refreshPage
* scrollPageToBottom
* scrollPageToTop
* grabBrowserLogs

add url decoding before assertion


Patch:
diff --git a/docs/advanced.md b/docs/advanced.md
index e8c78551..e133f29d 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -1,8 +1,53 @@
 # Advanced Usage
 
+## Data Driven Tests
+
+Execute the same scenario on a different data set.
+
+Let's say you want to test login for different user accounts.
+In this case, you need to create a datatable and fill it in with credentials.
+Then use `Data().Scenario` to include this data and generate multiple scenarios:
+
+```js
+// define data table inside a test or load from another module
+let accounts = new DataTable(['login', 'password']); //
+accounts.add(['davert', '123456']); // adding records to a table
+accounts.add(['admin', '123456']);
+
+// Pass dataTable to Data()
+// Use special param `current` to get current data set
+Data(accounts).Scenario('Test Login', (I, current) => {
+  I.fillField('Username', current.login); // current is reserved!
+  I.fillField('Password', current.password);
+  I.click('Sign In');
+  I.see('Welcome '+ current.login);
+});
+```
+
+*Important: you can't use name `current` for pageObjects or helpers in data scenarios*
+
+This will produce 2 tests with different data sets.
+Current data set is appended to a test name in output:
+
+```
+✓ Test Login | {"login":"davert","password":"123456"}
+✓ Test Login | {"login":"admin","password":"123456"}
+```
+
+Data sets can also be defined with array, generator, or a function.
+
+```js
+Data(function*() {
+  yield { user: 'davert'};
+  yield { user: 'andrey'};
+}).Scenario() // ...
+```
+
+*HINT: If you don't use DataTable. add `toString()` method to each object added to data set, so the data could be pretty printed in a test name*
+
 ## Groups
 
-Currently CodeceptJS doesn't support groups or tags. However, you can append `@tag` to your test name, so
+Append `@tag` to your test name, so
 all tests with `@tag` could be executed with `--grep @tag` option.
 
 ```js
diff --git a/docs/configuration.md b/docs/configuration.md
index 5fcb3332..6a492b2e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -43,7 +43,7 @@ exports.config = {
       key: process.env.CLOUDSERVICE_KEY,
 
       coloredLogs: true,
-      waitforTimeout: 10000
+      waitForTimeout: 10000
     }
   },
 
diff --git a/docs/data.md b/docs/data.md
index 4bdd769e..39ec26a5 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -1,5 +1,7 @@
 # Data Management
 
+*This chapter describes data management for external sources. If you are looking for using Data Sets in tests, see [Data Driven Tests](http://codecept.io/advanced/#data-drivern-tests) section*
+
 Managing data for tests is always a tricky issue. How isolate data between tests, how to prepare data for different tests, etc.
 There are different approaches to solve it:
 
@@ -21,7 +23,7 @@ However, it doesn't provide tools for testing APIs, so it should be paired with
 Enable REST helper in global config. It is recommended to set `endpoint`, a base URL for all API requests.
 If you need some authorization you can optionally set default headers too.
 
-See sample config.
+See the sample config:
 
 ```js
 "helpers": {
@@ -30,7 +32,7 @@ See sample config.
     "defaultHeaders": {
       "Auth": "11111"
     }
-  }
+  },
 
   "WebDriverIO" : {
     "browser": "chrome"
@@ -53,14 +55,24 @@ As well as a method for setting headers: `haveRequestHeaders`.
 Here is a usage example:
 
 ```js
-// valid access token
-I.haveRequestHeaders({auth: '1111111'});
-// get the first user
-let user = yield I.sendGetRequest('/api/users/1');
-// create a post
-I.sendPostRequest('/api/posts', { author: user.id, body: 'some text' });
-// open browser page of new post
-I.amOnPage('/posts/2.html');
+let postId = null;
+
+Scenario('check post page', function*(I) {
+  // valid access token
+  I.haveRequestHeaders({auth: '1111111'});
+  // get the first user
+  let user = yield I.sendGetRequest('/api/users/1');
+  // create a post and save its Id
+  postId = yield I.sendPostRequest('/api/posts', { author: user.id, body: 'some text' });
+  // open browser page of new post
+  I.amOnPage('/posts/2.html');
+  I.see('some text', 'p.body');
+});
+
+// cleanup created data
+After((I) => {
+  I.sendDeleteRequest('/api/posts/'+postId);
+});
 ```
 
 This can also be used to emulate Ajax requests:
@@ -69,6 +81,7 @@ This can also be used to emulate Ajax requests:
 I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest' });
 ```
 
+
 ## Data Generation with Factories
 
 This concept is extended by [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory/) helper.
diff --git a/lib/actor.js b/lib/actor.js
index fdf012a7..60325ccd 100644
--- a/lib/actor.js
+++ b/lib/actor.js
@@ -34,7 +34,7 @@ module.exports = function (obj) {
             step.actor = container.translation().I;
           }
           // add methods to promise chain
-          return recordStep(step, Array.prototype.slice.call(arguments));
+          return recordStep(step, Array.from(arguments));
         };
       });
     });
@@ -58,19 +58,19 @@ function recordStep(step, args) {
   // run step inside promise
   recorder.add(task, () => {
     event.emit(event.step.started, step);
-    step.startTime = +new Date();
+    step.startTime = Date.now();
     return val = step.run.apply(step, args);
   });
 
   event.emit(event.step.after, step);
 
   recorder.add(`step passed`, () => {
-    step.endTime = +new Date();
+    step.endTime = Date.now();
     event.emit(event.step.passed, step);
   });
 
   recorder.catchWithoutStop((err) => {
-    step.endTime = +new Date();
+    step.endTime = Date.now();
     event.emit(event.step.failed, step);
     throw err;
   });
diff --git a/lib/codecept.js b/lib/codecept.js
index ff1f5af5..12182c1a 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -41,11 +41,14 @@ class Codecept {
     // preparing globals
     global.codecept_dir = dir;
     global.output_dir = fsPath.resolve(dir, this.config.output);
-    global.actor = global.codecept_actor = require('./actor');
-    global.Helper = global.codecept_helper = require('./helper');
-    global.pause = require('./pause');
-    global.within = require('./within');
-    global.DataTable = require('./dataTable');
+
+    if (!this.config.noGlobals) {
+      global.actor = global.codecept_actor = require('./actor');
+      global.Helper = global.codecept_helper = require('./helper');
+      global.pause = require('./pause');
+      global.within = require('./within');
+      global.DataTable = require('./data/table');
+    }
 
     // initializing listeners
     Container.create(this.config, this.opts);
diff --git a/lib/container.js b/lib/container.js
index b80b3c9e..c46095d7 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -139,7 +139,6 @@ function createSupportObjects(config) {
     if (module.charAt(0) === '.') {
       module = path.join(global.codecept_dir, module);
     }
-    if (name.toLowerCase() === 'scenariocontext') throw new Error(`Unable to use scenarioContext as pageObject name. Please rename it.`);
     try {
       objects[name] = require(module);
     } catch (err) {
diff --git a/lib/data/context.js b/lib/data/context.js
new file mode 100644
index 00000000..0dfa6a2c
--- /dev/null
+++ b/lib/data/context.js
@@ -0,0 +1,50 @@
+const isGenerator = require('../utils').isGenerator;
+const DataTable = require('./table');
+
+module.exports = function (context) {
+
+  context.Data = function (dataTable) {
+    let data = detectDataType(dataTable);
+
+    return {
+      Scenario: function (title, opts, fn) {
+        if (typeof opts === 'function' && !fn) {
+          fn = opts;
+          opts = {};
+        }
+        data.forEach((dataRow) => {
+          opts.data = dataRow;
+          let test = context.Scenario(`${title} | ${dataRow}`, opts, fn);
+          test.inject.current = dataRow;
+        });
+      }
+    };
+  };
+
+  context.xData = function (dataTable) {
+    let data = detectDataType(dataTable);
+    return { Scenario: context.xScenario };
+  };
+};
+
+function detectDataType(dataTable) {
+  if (dataTable instanceof DataTable) {
+    return dataTable.rows;
+  }
+
+  if (isGenerator(dataTable)) {
+    let data = [];
+    for (let dataRow of dataTable()) {
+      data.push(dataRow);
+    }
+    return data;
+  }
+  if (typeof dataTable === 'function') {
+    return dataTable();
+  }
+  if (Array.isArray(dataTable)) {
+    return dataTable;
+  }
+
+  throw new Error('Invalid data type. Data accepts either: DataTable || generator || Array || function');
+}
diff --git a/lib/dataTable.js b/lib/data/table.js
similarity index 83%
rename from lib/dataTable.js
rename to lib/data/table.js
index 320acca0..c27f28ec 100644
--- a/lib/dataTable.js
+++ b/lib/data/table.js
@@ -7,7 +7,7 @@ class DataTable {
 
   constructor(array) {
     this.array = array;
-    this.datatable = [];
+    this.rows = [];
   }
 
   add(array) {
@@ -16,9 +16,10 @@ class DataTable {
     let arrayCounter = 0;
     this.array.forEach(function (elem) {
       tempObj[elem] = array[arrayCounter];
+      tempObj.toString = () => JSON.stringify(tempObj);
       arrayCounter++;
     });
-    this.datatable.push(tempObj);
+    this.rows.push(tempObj);
   }
 
 }
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index 41e9671a..e84ac5db 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -3,12 +3,15 @@ let webdriverio;
 const Helper = require('../helper');
 const stringIncludes = require('../assert/include').includes;
 const urlEquals = require('../assert/equal').urlEquals;
+const equals = require('../assert/equal').equals;
 const empty = require('../assert/empty').empty;
 const truth = require('../assert/truth').truth;
 const xpathLocator = require('../utils').xpathLocator;
 const hashCode = require('../utils').hashCode;
 const fileExists = require('../utils').fileExists;
 const clearString = require('../utils').clearString;
+const decodeUrl = require('../utils').decodeUrl;
+const chunkArray = require('../utils').chunkArray;
 const assert = require('assert');
 const path = require('path');
 const requireg = require('requireg');
@@ -703,6 +706,29 @@ class WebDriverIO extends Helper {
     });
   }
 
+  /**
+   * Grab CSS property for given locator
+   *
+   * ```js
+   * I.grabCssPropertyFrom('h3', 'font-weight');
+   * ```
+   */
+  grabCssPropertyFrom(locator, cssProperty) {
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let commands = [];
+      res.value.forEach((el) => commands.push(client.elementIdCssProperty(el.ELEMENT, cssProperty)));
+      return client.unify(commands, {
+        extractValue: true
+      }).then((selected) => {
+        return selected;
+      });
+    });
+  }
+
   /**
    * {{> ../webapi/grabAttributeFrom }}
    * Appium: can be used for apps only with several values ("contentDescription", "text", "className", "resourceId")
@@ -733,6 +759,19 @@ class WebDriverIO extends Helper {
     });
   }
 
+  /**
+   * Checks that title is equal to provided one.
+   *
+   * ```js
+   * I.seeTitleEquals('Test title.');
+   * ```
+   */
+  seeTitleEquals(text) {
+    return this.browser.getTitle().then((title) => {
+      return assert.equal(title, text, `expected web page title to be ${text}, but found ${title}`);
+    });
+  }
+
   /**
    * {{> ../webapi/dontSeeInTitle }}
    * Appium: support only web testing
@@ -762,6 +801,17 @@ class WebDriverIO extends Helper {
     return proceedSee.call(this, 'assert', text, context);
   }
 
+  /**
+   * Checks that text is equal to provided one.
+   *
+   * ```js
+   * I.seeTextEquals('text', 'h1');
+   * ```
+   */
+  seeTextEquals(text, context = null) {
+    return proceedSeeEquals.call(this, 'assert', text, context);
+  }
+
   /**
    * {{> ../webapi/dontSee }}
    * Appium: support with context in apps
@@ -878,6 +928,20 @@ class WebDriverIO extends Helper {
     return this.browser.getSource();
   }
 
+  /**
+   * Get JS log from browser. Log buffer is reset after each request.
+   *
+   * ```js
+   * let logs = yield I.grabBrowserLogs();
+   * console.log(JSON.stringify(logs))
+   * ```
+   */
+  grabBrowserLogs() {
+    return this.browser.log("browser").then((res) => {
+      return res.value;
+    });
+  }
+
   /**
    * {{> ../webapi/dontSeeInSource }}
    * Appium: support
@@ -900,7 +964,8 @@ class WebDriverIO extends Helper {
   seeNumberOfElements(selector, num) {
     return this.browser.elements(withStrictLocator(selector))
       .then(function (res) {
-        return assert.equal(res.value.length, num, `expected nummber of elements (${selector}) is ${num}, but found ${res.value.length}`);
+        return assert.equal(res.value.length, num,
+          `expected number of elements (${selector}) is ${num}, but found ${res.value.length}`);
       });
   }
 
@@ -913,9 +978,103 @@ class WebDriverIO extends Helper {
    * ```
    */
   seeNumberOfVisibleElements(locator, num) {
+    return this.grabNumberOfVisibleElements.then(function (res) {
+      return assert.equal(res, num,
+        `expected number of visible elements (${locator}) is ${num}, but found ${res}`);
+    });
+  }
+
+  /**
+   * Checks that all elements with given locator have given CSS properties.
+   *
+   * ```js
+   * I.seeCssPropertiesOnElements('h3', { 'font-weight': "bold"});
+   * ```
+   */
+  seeCssPropertiesOnElements(locator, cssProperties) {
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let elemAmount = res.value.length;
+      let commands = [];
+      res.value.forEach((el) => {
+        Object.keys(cssProperties).forEach((prop) => {
+          commands.push(client.elementIdCssProperty(el.ELEMENT, prop));
+        });
+      });
+      return client.unify(commands, {
+        extractValue: true
+      }).then((props) => {
+        let values = Object.keys(cssProperties).map(function (key){
+          return cssProperties[key];
+        });
+        if (!Array.isArray(props)) props = [props];
+        let chunked = chunkArray(props, values.length);
+        chunked = chunked.filter((val) => {
+          for (var i = 0; i < val.length; ++i) {
+            if (val[i] !== values[i]) return false;
+          }
+          return true;
+        });
+        return assert.ok(chunked.length === elemAmount,
+          `Not all elements (${locator}) have CSS property ${JSON.stringify(cssProperties)}`);
+      });
+    });
+  }
+
+  /**
+   * Checks that all elements with given locator have given attributes.
+   *
+   * ```js
+   * I.seeAttributesOnElements('//form', {'method': "post"});
+   * ```
+   */
+  seeAttributesOnElements(locator, attributes) {
+    let client = this.browser;
+    return this._locate(locator).then((res) => {
+      if (!res.value || res.value.length === 0) {
+        throw new Error(`${locator} cant be located by name|text|CSS|XPath`);
+      }
+      let elemAmount = res.value.length;
+      let commands = [];
+      res.value.forEach((el) => {
+        Object.keys(attributes).forEach((attr) => {
+          commands.push(client.elementIdAttribute(el.ELEMENT, attr));
+        });
+      });
+      return client.unify(commands, {
+        extractValue: true
+      }).then((attrs) => {
+        let values = Object.keys(attributes).map(function (key){
+          return attributes[key];
+        });
+        if (!Array.isArray(attrs)) attrs = [attrs];
+        let chunked = chunkArray(attrs, values.length);
+        chunked = chunked.filter((val) => {
+          for (var i = 0; i < val.length; ++i) {
+            if (val[i] !== values[i]) return false;
+          }
+          return true;
+        });
+        return assert.ok(chunked.length === elemAmount,
+          `Not all elements (${locator}) have attributes ${JSON.stringify(attributes)}`);
+      });
+    });
+  }
+
+  /**
+   * Grab number of visible elements by locator
+   *
+   * ```js
+   * I.grabNumberOfVisibleElements('p');
+   * ```
+   */
+  grabNumberOfVisibleElements(locator) {
     return this.browser.elements(locator).then(function (res) {
       if (!res.value || res.value.length === 0) {
-        return truth(`elements of ${selector}`, 'to be seen').assert(false);
+        return 0;
       }
       let commands = [];
       res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
@@ -924,7 +1083,7 @@ class WebDriverIO extends Helper {
       }).then((selected) => {
         if (!Array.isArray(selected)) selected = [selected];
         selected = selected.filter((val) => val === true);
-        return assert.equal(selected.length, num, `expected nummber of visible elements (${locator}) is ${num}, but found ${selected.length}`);
+        return selected.length;
       });
     });
   }
@@ -935,7 +1094,7 @@ class WebDriverIO extends Helper {
    */
   seeInCurrentUrl(url) {
     return this.browser.url().then(function (res) {
-      return stringIncludes('url').assert(url, res.value);
+      return stringIncludes('url').assert(url, decodeUrl(res.value));
     });
   }
 
@@ -945,7 +1104,7 @@ class WebDriverIO extends Helper {
    */
   dontSeeInCurrentUrl(url) {
     return this.browser.url().then(function (res) {
-      return stringIncludes('url').negate(url, res.value);
+      return stringIncludes('url').negate(url, decodeUrl(res.value));
     });
   }
 
@@ -955,7 +1114,7 @@ class WebDriverIO extends Helper {
    */
   seeCurrentUrlEquals(url) {
     return this.browser.url().then((res) => {
-      return urlEquals(this.options.url).assert(url, res.value);
+      return urlEquals(this.options.url).assert(url, decodeUrl(res.value));
     });
   }
 
@@ -965,7 +1124,7 @@ class WebDriverIO extends Helper {
    */
   dontSeeCurrentUrlEquals(url) {
     return this.browser.url().then((res) => {
-      return urlEquals(this.options.url).negate(url, res.value);
+      return urlEquals(this.options.url).negate(url, decodeUrl(res.value));
     });
   }
 
@@ -1016,7 +1175,7 @@ class WebDriverIO extends Helper {
         return this.elementIdLocation(elem.ELEMENT).then(function (location) {
           if (!location.value || location.value.length === 0) {
             throw new Error(
-            `Failed to recieve (${locator}) location`);
+              `Failed to recieve (${locator}) location`);
           }
           return this.execute(function scroll(x, y) {
             return window.scrollTo(x, y);
@@ -1053,7 +1212,7 @@ class WebDriverIO extends Helper {
           return this.elementIdLocation(elem.ELEMENT).then(function (location) {
             if (!location.value || location.value.length === 0) {
               throw new Error(
-              `Failed to recieve (${locator}) location`);
+                `Failed to recieve (${locator}) location`);
             }
             var x = location.value.x + size.value.width / 2;
             var y = location.value.y + size.value.height / 2;
@@ -1260,20 +1419,20 @@ class WebDriverIO extends Helper {
         return this.elementIdLocation(elem.ELEMENT).then(function (location) {
           if (!location.value || location.value.length === 0) {
             throw new Error(
-            `Failed to recieve (${srcElement}) location`);
+              `Failed to recieve (${srcElement}) location`);
           }
           return this.touchDown(location.value.x, location.value.y).then(function (res) {
             if (res.state !== 'success') throw new Error(`Failed to touch button down on (${srcElement})`);
             return client.element(withStrictLocator(destElement)).then(function (res) {
               if (!res.value || res.value.length === 0) {
                 return truth(`elements of ${destElement}`, 'to be seen')
-                .assert(false);
+                  .assert(false);
               }
               elem = res.value;
               return this.elementIdLocation(elem.ELEMENT).then(function (location) {
                 if (!location.value || location.value.length === 0) {
                   throw new Error(
-                  `Failed to recieve (${destElement}) location`);
+                    `Failed to recieve (${destElement}) location`);
                 }
                 return this.touchMove(location.value.x, location.value.y).then(function (res) {
                   if (res.state !== 'success') throw new Error(`Failed to touch move to (${destElement})`);
@@ -1367,6 +1526,64 @@ class WebDriverIO extends Helper {
     }, sec * 1000, `element (${locator}) still not present on page after ${sec} sec`);
   }
 
+
+  /**
+   * Waiting for the part of the URL to match the expected. Useful for SPA to understand that page was changed.
+   *
+   * ```js
+   * I.waitInUrl('/info', 2);
+   * ```
+   */
+  waitInUrl(urlPart, sec = null) {
+    let client = this.browser;
+    sec = sec || this.options.waitForTimeout;
+    let currUrl = "";
+    return client
+      .waitUntil(function () {
+        return this.url().then(function (res) {
+          currUrl = decodeUrl(res.value);
+          return currUrl.indexOf(urlPart) > -1;
+        });
+      }, sec * 1000).catch((e) => {
+        if (e.type === 'WaitUntilTimeoutError') {
+          throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`);
+        } else {
+          throw e;
+        }
+      });
+  }
+
+  /**
+   * Waits for the entire URL to match the expected
+   *
+   * ```js
+   * I.waitUrlEquals('/info', 2);
+   * I.waitUrlEquals('http://127.0.0.1:8000/info');
+   * ```
+   */
+  waitUrlEquals(urlPart, sec = null) {
+    let client = this.browser;
+    sec = sec || this.options.waitForTimeout;
+    let baseUrl = this.options.url;
+    if (urlPart.indexOf('http') < 0) {
+      urlPart = baseUrl + urlPart;
+    }
+    let currUrl = "";
+    return client
+      .waitUntil(function () {
+        return this.url().then(function (res) {
+          currUrl = decodeUrl(res.value);
+          return currUrl === urlPart;
+        });
+      }, sec * 1000).catch((e) => {
+        if (e.type === 'WaitUntilTimeoutError') {
+          throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`);
+        } else {
+          throw e;
+        }
+      });
+  }
+
   /**
    * {{> ../webapi/waitForText }}
    * Appium: support
@@ -1395,6 +1612,36 @@ class WebDriverIO extends Helper {
       `element (${context}) is not in DOM or there is no element(${context}) with text "${text}" after ${sec} sec`);
   }
 
+  /**
+   * Waits for the specified value to be in value attribute
+   *
+   * ```js
+   * I.waitForValue("GoodValue", '//input');
+   * ```
+   */
+  waitForValue(context, value, sec = null) {
+    let client = this.browser;
+    sec = sec || this.options.waitForTimeout;
+    return client.waitUntil(function () {
+      return findFields(client, context).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
+        }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdAttribute(el.ELEMENT, "value")));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (Array.isArray(selected)) {
+            return selected.filter(part => part.indexOf(value) >= 0).length > 0;
+          }
+          return selected.indexOf(value) >= 0;
+        });
+      });
+    }, sec * 1000,
+      `element (${context}) is not in DOM or there is no element(${context}) with value "${value}" after ${sec} sec`);
+  }
+
   /**
    * {{> ../webapi/waitForVisible }}
    * Appium: support
@@ -1421,6 +1668,33 @@ class WebDriverIO extends Helper {
     }, sec * 1000, `element (${locator}) still not visible after ${sec} sec`);
   }
 
+  /**
+   * Waits for a specified number of elements on the page
+   *
+   * ```js
+   * I.waitNumberOfVisibleElements('a', 3);
+   * ```
+   */
+  waitNumberOfVisibleElements(locator, num, sec) {
+    let client = this.browser;
+    sec = sec || this.options.waitForTimeout;
+    return client.waitUntil(function () {
+      return client.elements(withStrictLocator(locator)).then(function (res) {
+        if (!res.value || res.value.length === 0) {
+          return false;
+        }
+        let commands = [];
+        res.value.forEach((el) => commands.push(this.elementIdDisplayed(el.ELEMENT)));
+        return this.unify(commands, {
+          extractValue: true
+        }).then((selected) => {
+          if (!Array.isArray(selected)) selected = [selected];
+          return selected.length === num;
+        });
+      });
+    }, sec * 1000, `The number of elements ${locator} is not ${num} after ${sec} sec`);
+  }
+
   /**
    * {{> ../webapi/waitForInvisible }}
    * Appium: support
@@ -1496,6 +1770,119 @@ class WebDriverIO extends Helper {
     });
   }
 
+  /**
+   * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
+   *
+   * ```js
+   * I.switchToNextTab();
+   * I.switchToNextTab(2);
+   * ```
+   */
+  switchToNextTab(num = 1, sec = null) {
+    sec = sec || this.options.waitForTimeout;
+    let client = this.browser;
+    return client
+      .waitUntil(function () {
+        return this.getTabIds().then(function (handles) {
+          return this.getCurrentTabId().then(function (current) {
+            if (handles.indexOf(current) + num + 1 <= handles.length) {
+              return this.switchTab(
+              handles[handles.indexOf(current) + num]);
+            } else return false;
+          });
+        });
+      }, sec * 1000, `There is no ability to switch to next tab with offset ${num}`);
+  }
+
+  /**
+   * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
+   *
+   * ```js
+   * I.switchToPreviousTab();
+   * I.switchToPreviousTab(2);
+   * ```
+   */
+  switchToPreviousTab(num = 1, sec = null) {
+    sec = sec || this.options.waitForTimeout;
+    let client = this.browser;
+    return client
+      .waitUntil(function () {
+        return this.getTabIds().then(function (handles) {
+          return this.getCurrentTabId().then(function (current) {
+            if (handles.indexOf(current) - num > -1) return this.switchTab(handles[handles.indexOf(current) - num]);
+            else return false;
+          });
+        });
+      }, sec * 1000, `There is no ability to switch to previous tab with offset ${num}`);
+  }
+
+  /**
+   * Close current tab
+   *
+   * ```js
+   * I.closeCurrentTab();
+   * ```
+   */
+  closeCurrentTab() {
+    let client = this.browser;
+    return client.close();
+  }
+
+  /**
+   * Open new tab and switch to it
+   *
+   * ```js
+   * I.openNewTab();
+   * ```
+   */
+  openNewTab() {
+    let client = this.browser;
+    return client.newWindow('about:blank');
+  }
+
+  /**
+   * Refresh the current page.
+   *
+   * ```js
+   * I.refreshPage();
+   * ```
+   */
+  refreshPage() {
+    let client = this.browser;
+    return client.refresh();
+  }
+
+  /**
+   * Scroll page to the top
+   *
+   * ```js
+   * I.scrollPageToTop();
+   * ```
+   */
+  scrollPageToTop() {
+    let client = this.browser;
+    return client.execute(function () {
+      window.scrollTo(0, 0);
+    });
+  }
+
+  /**
+   * Scroll page to the bottom
+   *
+   * ```js
+   * I.scrollPageToBottom();
+   * ```
+   */
+  scrollPageToBottom() {
+    let client = this.browser;
+    return client.execute(function () {
+      var body = document.body,
+        html = document.documentElement;
+      window.scrollTo(0, Math.max(body.scrollHeight, body.offsetHeight,
+        html.clientHeight, html.scrollHeight, html.offsetHeight));
+    });
+  }
+
 }
 
 function proceedSee(assertType, text, context) {
@@ -1529,6 +1916,37 @@ function proceedSee(assertType, text, context) {
   });
 }
 
+function proceedSeeEquals(assertType, text, context) {
+  let description;
+  if (!context) {
+    if (this.context === webRoot) {
+      context = this.context;
+      description = 'web page';
+    } else {
+      description = 'current context ' + this.context;
+      context = './/*';
+    }
+  } else {
+    description = 'element ' + context;
+  }
+
+  let client = this.browser;
+
+  return client.elements(withStrictLocator(context)).then(function (res) {
+    if (!res.value || res.value.length === 0) {
+      throw new Error(`${context} can't be located by name|text|CSS|XPath`);
+    }
+
+    let commands = [];
+    res.value.forEach((el) => commands.push(this.elementIdText(el.ELEMENT)));
+    return this.unify(commands, {
+      extractValue: true
+    }).then((selected) => {
+      return equals(description)[assertType](text, selected);
+    });
+  });
+}
+
 function findClickable(client, locator) {
   if (typeof locator === 'object') return client.elements(withStrictLocator(locator));
   if (isCSSorXPathLocator(locator)) return client.elements(locator);
diff --git a/lib/index.js b/lib/index.js
index d477c7db..7f794013 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -15,5 +15,5 @@ module.exports = {
   helper: require('./helper'),
   pause: require('./pause'),
   within: require('./within'),
-  dataTable: require('./dataTable')
+  dataTable: require('./data/table')
 };
diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js
index c8c5af46..b2d6f868 100644
--- a/lib/interfaces/bdd.js
+++ b/lib/interfaces/bdd.js
@@ -9,6 +9,8 @@ var scenario = require('../scenario');
 var recorder = require('../recorder');
 var escapeRe = require('escape-string-regexp');
 
+const addDataContext = require('../data/context');
+
 /**
  * Codecept-style interface:
  *
@@ -28,30 +30,10 @@ module.exports = function (suite) {
 
   suite.on('pre-require', function (context, file, mocha) {
     var common = require('mocha/lib/interfaces/common')(suites, context);
-    let addScenario = function (title, data, opts, fn, testData) {
+
+    let addScenario = function (title, opts, fn) {
       var suite = suites[0];
 
-      if (data instanceof DataTable) {
-        if (typeof opts === 'function' && !fn) {
-          fn = opts;
-          opts = {};
-        }
-        data.datatable.forEach(function (scenario) {
-          addScenario(`${title} ${JSON.stringify(scenario)}`, opts, fn, scenario);
-        });
-        return;
-      } else {
-        testData = fn;
-        fn = opts;
-        opts = data;
-        data = {};
-        if (suite.codeceptData && !testData) {
-          suite.codeceptData.datatable.forEach(function (scenario) {
-            addScenario(`${title} ${JSON.stringify(scenario)}`, opts, fn, scenario);
-          });
-          return;
-        }
-      }
       if (typeof opts === 'function' && !fn) {
         fn = opts;
         opts = {};
@@ -78,9 +60,9 @@ module.exports = function (suite) {
       var test = new Test(title, fn);
       test.fullTitle = () => `${suite.title}: ${title}`;
       test.file = file;
-      test.codeceptData = testData;
       test.async = true;
       test.timeout(0);
+      test.inject = {};
 
       if (opts.retries) test.retries(opts.retries);
       if (opts.timeout) test.timeout(opts.timeout);
@@ -102,7 +84,7 @@ module.exports = function (suite) {
      * and/or tests.
      */
 
-    context.Feature = function (title, data, opts) {
+    context.Feature = function (title, opts) {
       if (suites.length > 1) {
         suites.shift();
       }
@@ -113,11 +95,6 @@ module.exports = function (suite) {
       afterEachHooksAreLoaded = false;
 
       var suite = Suite.create(suites[0], title);
-      if (data instanceof DataTable) {
-        suite.codeceptData = data;
-      } else {
-        opts = data;
-      }
       if (!opts) opts = {};
       suite.timeout(0);
 
@@ -157,7 +134,6 @@ module.exports = function (suite) {
      * acting as a thunk.
      */
     context.Scenario = addScenario;
-
     /**
      * Exclusive test-case.
      */
@@ -173,5 +149,7 @@ module.exports = function (suite) {
     context.xScenario = context.Scenario.skip = function (title) {
       context.Scenario(title, {});
     };
+
+    addDataContext(context);
   });
 };
diff --git a/lib/mocha_factory.js b/lib/mocha_factory.js
index 79176e92..0f652b88 100644
--- a/lib/mocha_factory.js
+++ b/lib/mocha_factory.js
@@ -2,6 +2,7 @@ const Mocha = require('mocha/lib/mocha');
 const fsPath = require('path');
 const reporter = require('./reporter/cli');
 const scenarioUi = fsPath.join(__dirname, './interfaces/bdd.js');
+const output = require('../lib/output');
 
 let mocha;
 
@@ -9,7 +10,7 @@ class MochaFactory {
 
   static create(config, opts) {
     mocha = new Mocha(Object.assign(config, opts));
-
+    output.process(opts.child);
     mocha.ui(scenarioUi);
     // use standard reporter
     if (!opts.reporter) {
diff --git a/lib/reporter/cli.js b/lib/reporter/cli.js
index e6e80b27..b26914f4 100644
--- a/lib/reporter/cli.js
+++ b/lib/reporter/cli.js
@@ -18,7 +18,6 @@ class Cli extends Base {
     if (opts.debug) level = 2;
     if (opts.verbose) level = 3;
 
-    output.process(opts.child);
     output.level(level);
     output.print('CodeceptJS v' + require('../codecept').version());
     output.print(`Using test root "${global.codecept_dir}"`);
diff --git a/lib/scenario.js b/lib/scenario.js
index 0d5b720b..6149fd2f 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -126,9 +126,11 @@ function getInjectedArguments(fn, test) {
   let objects = container.support();
   for (var key in params) {
     let obj = params[key];
-    if (obj === 'scenarioContext') {
-      testArguments.push(test.codeceptData);
-    } else if (!objects[obj]) {
+    if (test.inject[obj]) {
+      testArguments.push(test.inject[obj]);
+      continue;
+    }
+    if (!objects[obj]) {
       throw new Error(`Object of type ${obj} is not defined in container`);
     }
     testArguments.push(container.support(obj));
diff --git a/lib/utils.js b/lib/utils.js
index 6a57e3cb..d666e563 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -124,6 +124,16 @@ module.exports.hashCode = function (str) {
   return hash;
 };
 
+module.exports.chunkArray = function (arr, chunk) {
+  var i;
+  var j;
+  var tmp = [];
+  for (i = 0, j = arr.length; i < j; i += chunk) {
+    tmp.push(arr.slice(i, i + chunk));
+  }
+  return tmp;
+};
+
 module.exports.clearString = function (str) {
   /* Replace forbidden symbols in string
    */
@@ -140,6 +150,12 @@ module.exports.clearString = function (str) {
     .replace(/\*/g, '^');
 };
 
+module.exports.decodeUrl = function (url) {
+  /* Replace forbidden symbols in string
+   */
+  return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)));
+};
+
 module.exports.xpathLocator = {
   literal: (string) => {
     if (string.indexOf("'") > -1) {


Test patch:
diff --git a/test/data/app/view/index.php b/test/data/app/view/index.php
index f41fa32d..2991adb8 100755
--- a/test/data/app/view/index.php
+++ b/test/data/app/view/index.php
@@ -4,21 +4,21 @@
 
 <h1>Welcome to test app!</h1>
 
-<div class="notice"><?php if (isset($notice)) echo $notice; ?></div>
+<div class="notice" qa-id = "test"><?php if (isset($notice)) echo $notice; ?></div>
 
 <p>
-    <a href="/info" id="link">More info</a>
+    <a href="/info" id="link" qa-id = "test" qa-link = "test">More info</a>
 </p>
 
 
-<div id="area1">
-    <a href="/form/file"> Test Link </a>
+<div id="area1" qa-id = "test" qa-id = "test">
+    <a href="/form/file" qa-id = "test" qa-link = "test"> Test Link </a>
 </div>
-<div id="area2">
-    <a href="/form/hidden">Test</a>
+<div id="area2" qa-id = "test">
+    <a href="/form/hidden" qa-id = "test" qa-link = "test">Test</a>
 </div>
-<div id="area3">
-    <a href="info">Document-Relative Link</a>
+<div id="area3" qa-id = "test">
+    <a href="info" qa-id = "test" qa-link = "test">Document-Relative Link</a>
 </div>
 
 A wise man said: "debug!"
diff --git a/test/data/app/view/info.php b/test/data/app/view/info.php
index d57cda72..ddbde925 100755
--- a/test/data/app/view/info.php
+++ b/test/data/app/view/info.php
@@ -41,5 +41,9 @@
     <a id="third-link">Third</a>
 </div>
 
+<div id="new-tab">
+<a href="/login" target="_blank">New tab</a>
+</div>
+
 </body>
 </html>
diff --git a/test/data/sandbox/ddt_test.ddt.js b/test/data/sandbox/ddt_test.ddt.js
index cf71b3c7..4498a908 100644
--- a/test/data/sandbox/ddt_test.ddt.js
+++ b/test/data/sandbox/ddt_test.ddt.js
@@ -1,18 +1,24 @@
+Feature('DDT');
+
 let accounts1 = new DataTable(['login', 'password']);
 accounts1.add(['davert', '123456']);
 accounts1.add(['admin', '666666']);
 
-Feature('DDT', accounts1);
-
 let accounts2 = new DataTable(['login', 'password']);
 accounts2.add(['andrey', '555555']);
 accounts2.add(['collaborator', '222222']);
 
-Scenario('Should log accounts1', (scenarioContext) => {
-  console.log(`Got login ${scenarioContext.login} and password ${scenarioContext.password}`);
+Data(accounts1).Scenario('Should log accounts1', (I, current) => {
+  console.log(`Got login ${current.login} and password ${current.password}`);
 });
 
-Scenario('Should log accounts2', accounts2, (scenarioContext) => {
-  console.log(`Got changed login ${scenarioContext.login} and password ${scenarioContext.password}`);
+Data(accounts2).Scenario('Should log accounts2', (I, current) => {
+  console.log(`Got changed login ${current.login} and password ${current.password}`);
 });
 
+Data(function*() {
+  yield ['nick', 'pick'];
+  yield ['jack', 'sacj'];
+}).Scenario('Should log accounts3', (I, current) => {
+  console.log(`Got changed login ${current[0]}`);
+});
diff --git a/test/helper/WebDriverIO_test.js b/test/helper/WebDriverIO_test.js
index b1690570..0b53d976 100644
--- a/test/helper/WebDriverIO_test.js
+++ b/test/helper/WebDriverIO_test.js
@@ -167,6 +167,190 @@ describe('WebDriverIO', function () {
     });
   });
 
+  describe('#seeAttributesOnElements', () => {
+    it('should check attributes values for given element', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.seeAttributesOnElements('//form', { method: "post"}))
+        .then(() => wd.seeAttributesOnElements('//form', { method: "post", action: "http://127.0.0.1:8000/"}))
+        .then(() => wd.seeAttributesOnElements('//form', { method: "get"}))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `Not all elements (//form) have attributes {"method":"get"}`);
+        });
+    });
+
+    it('should check attributes values for several elements', () => {
+      return wd.amOnPage('/')
+        .then(() => wd.seeAttributesOnElements('a', { 'qa-id': "test", 'qa-link': 'test'}))
+        .then(() => wd.seeAttributesOnElements('//div', { 'qa-id': 'test'}))
+        .then(() => wd.seeAttributesOnElements('a', { 'qa-id': "test", href: '/info'}))
+        .then(expectError)
+        .catch((e) => {
+          e.message.should.include(`Not all elements (a) have attributes {"qa-id":"test","href":"/info"}`);
+        });
+    });
+  });
+
+  describe('#seeTitleEquals', () => {
+    it('should check that title is equal to provided one', () => {
+      return wd.amOnPage('/')
+        .then(() => wd.seeTitleEquals('TestEd Beta 2.0'))
+        .then(() => wd.seeTitleEquals('TestEd Beta 2.'))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `expected web page title to be TestEd Beta 2., but found TestEd Beta 2.0`);
+        });
+    });
+  });
+
+  describe('#seeTextEquals', () => {
+    it('should check text is equal to provided one', () => {
+      return wd.amOnPage('/')
+        .then(() => wd.seeTextEquals('Welcome to test app!', 'h1'))
+        .then(() => wd.seeTextEquals('Welcome to test app', 'h1'))
+        .then(expectError)
+        .catch((e) => {
+          e.should.be.instanceOf(AssertionFailedError);
+          e.inspect().should.include("expected element h1 'Welcome to test app' to equal 'Welcome to test app!'");
+        });
+    });
+  });
+
+  describe('#grabCssPropertyFrom', () => {
+    it('should grab css property for given element', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.grabCssPropertyFrom('h3', 'font-weight'))
+        .then((css) => assert.equal(css, "bold"));
+    });
+  });
+
+  describe('#seeCssPropertiesOnElements', () => {
+    it('should check css property for given element', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': "bold"}))
+        .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': "bold", display: 'block'}))
+        .then(() => wd.seeCssPropertiesOnElements('h3', { 'font-weight': "non-bold"}))
+        .then(expectError)
+        .catch((e) => {
+          e.message.should.include(`Not all elements (h3) have CSS property {"font-weight":"non-bold"}`);
+        });
+    });
+
+    it('should check css property for several elements', () => {
+      return wd.amOnPage('/')
+        .then(() => wd.seeCssPropertiesOnElements('a', { color: "rgba(0, 0, 238, 1)", cursor: 'auto'}))
+        .then(() => wd.seeCssPropertiesOnElements('//div', { display: 'block'}))
+        .then(() => wd.seeCssPropertiesOnElements('a', { 'margin-top': "0em", cursor: 'auto'}))
+        .then(expectError)
+        .catch((e) => {
+          e.message.should.include(`Not all elements (a) have CSS property {"margin-top":"0em","cursor":"auto"}`);
+        });
+    });
+  });
+
+  describe('#grabNumberOfVisibleElements', () => {
+    it('should grab number of visible elements for given locator', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.grabNumberOfVisibleElements('//div[@id = "grab-multiple"]//a'))
+        .then((num) => assert.equal(num, 3));
+    });
+  });
+
+  describe('#waitInUrl, #waitUrlEquals', () => {
+    it('should wait part of the URL to match the expected', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.waitInUrl('/info'))
+        .then(() => wd.waitInUrl('/info2', 0.1))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `expected url to include /info2, but found http://127.0.0.1:8000/info`);
+        });
+    });
+    it('should wait for the entire URL to match the expected', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.waitUrlEquals('/info'))
+        .then(() => wd.waitUrlEquals('http://127.0.0.1:8000/info'))
+        .then(() => wd.waitUrlEquals('/info2', 0.1))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `expected url to be http://127.0.0.1:8000/info2, but found http://127.0.0.1:8000/info`);
+        });
+    });
+  });
+
+  describe('#waitForValue', () => {
+    it('should wait for expected value for given locator', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.waitForValue('//input[@name= "rus"]', "Верно"))
+        .then(() => wd.waitForValue('//input[@name= "rus"]', "Верно3", 0.1))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `element (//input[@name= "rus"]) is not in DOM or there is no element(//input[@name= "rus"]) with value "Верно3" after 0.1 sec`);
+        });
+    });
+  });
+
+  describe('#waitNumberOfVisibleElements', () => {
+    it('should wait for a specified number of elements on the page', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 3))
+        .then(() => wd.waitNumberOfVisibleElements('//div[@id = "grab-multiple"]//a', 2, 0.1))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, `The number of elements //div[@id = "grab-multiple"]//a is not 2 after 0.1 sec`);
+        });
+    });
+  });
+
+  describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab', () => {
+    it('should switch to next tab', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.click('New tab'))
+        .then(() => wd.switchToNextTab())
+        .then(() => wd.waitInUrl('/login'));
+    });
+    it('should assert when there is no ability to switch to next tab', () => {
+      return wd.amOnPage('/')
+        .then(() => wd.click('More info'))
+        .then(() => wd.switchToNextTab(2))
+        .then(expectError)
+        .catch((e) => {
+        assert.equal(e.message, "There is no ability to switch to next tab with offset 2");
+        });
+    });
+    it('should close current tab', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.click('New tab'))
+        .then(() => wd.switchToNextTab())
+        .then(() => wd.waitInUrl('/login'))
+        .then(() => wd.closeCurrentTab())
+        .then(() => wd.waitInUrl('/info'));
+    });
+    it('should open new tab', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.openNewTab())
+        .then(() => wd.waitInUrl('about:blank'));
+    });
+    it('should switch to previous tab', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.openNewTab())
+        .then(() => wd.waitInUrl('about:blank'))
+        .then(() => wd.switchToPreviousTab())
+        .then(() => wd.waitInUrl('/info'));
+    });
+    it('should assert when there is no ability to switch to previous tab', () => {
+      return wd.amOnPage('/info')
+        .then(() => wd.openNewTab())
+        .then(() => wd.waitInUrl('about:blank'))
+        .then(() => wd.switchToPreviousTab(2))
+        .then(() => wd.waitInUrl('/info'))
+        .then(expectError)
+        .catch((e) => {
+          assert.equal(e.message, "There is no ability to switch to previous tab with offset 2");
+        });
+    });
+  });
+
   describe('popup : #acceptPopup, #seeInPopup, #cancelPopup', () => {
     it('should accept popup window', () => {
       return wd.amOnPage('/form/popup')
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 8fa59f8e..dc146e21 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -13,7 +13,7 @@ let fs;
 describe('CodeceptJS Interface', () => {
 
   before(() => {
-    global.codecept_dir = path.join(__dirname, '/../data/sandbox');
+    process.chdir(codecept_dir);
   });
 
   it('should rerun flacky tests', (done) => {
@@ -28,17 +28,25 @@ describe('CodeceptJS Interface', () => {
   it('should run tests with different data', (done) => {
     exec(config_run_config('codecept.ddt.json'), (err, stdout, stderr) => {
       var output = stdout.replace(/in [0-9]ms/g, "").replace(/\r/g, "")
-      output.should.include(
-        `DDT --
-Got login davert and password 123456
- ✓ Should log accounts1 {"login":"davert","password":"123456"} 
-Got login admin and password 666666
- ✓ Should log accounts1 {"login":"admin","password":"666666"} 
-Got changed login andrey and password 555555
- ✓ Should log accounts2 {"login":"andrey","password":"555555"} 
-Got changed login collaborator and password 222222
- ✓ Should log accounts2 {"login":"collaborator","password":"222222"}`
-      )
+      output.should.include(`Got login davert and password 123456
+ ✓ Should log accounts1 | {"login":"davert","password":"123456"}`);
+
+      output.should.include(`Got login admin and password 666666
+ ✓ Should log accounts1 | {"login":"admin","password":"666666"}`);
+
+      output.should.include(`Got changed login andrey and password 555555
+ ✓ Should log accounts2 | {"login":"andrey","password":"555555"}`);
+
+      output.should.include(`Got changed login collaborator and password 222222
+ ✓ Should log accounts2 | {"login":"collaborator","password":"222222"}`);
+
+     output.should.include(
+`Got changed login nick
+ ✓ Should log accounts3 | nick`);
+
+     output.should.include(`Got changed login jack
+ ✓ Should log accounts3 | jack`);
+
       assert(!err);
       done();
     });