Repository: Codeception/CodeceptJS
Pull request #466 (2017-04-11 00:20:45)

Run and run-multiple commands refactored
Replaced directory path for `run` command with config option

* `codecept run .` => `codecept run .`
* `codecept run examples` => `codecept run --config examples`

Changed config syntax for `run-multiple` command

```js
  "multiple": {
    "default": {
      "grep": "signin",
      "browsers": ["chrome", { "browser": "firefox", "windowSize":"maximize"}]
    }
  },
```

* better output for child processes
* copy options to child processes
* added `--all` option to run all suites
* `grep` is optional param for suite config
* added tests 


Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index de61613e..2901f8c2 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -50,7 +50,7 @@ program.command('generate:helper [path]')
   .description('Generates a new helper')
   .action(require('../lib/command/generate').helper);
 
-program.command('run [suite] [test]')
+program.command('run [test]')
   .description('Executes tests')
 
   // codecept-only options
@@ -59,11 +59,11 @@ program.command('run [suite] [test]')
   .option('--verbose', 'output internal logging information')
   .option('-o, --override [value]', 'override current config options')
   .option('--profile [value]', 'configuration profile to be used')
-  .option('--config [file]', 'configuration file to be used')
+  .option('-c, --config [file]', 'configuration file to be used')
 
   // mocha options
-  .option('-c, --colors', 'force enabling of colors')
-  .option('-C, --no-colors', 'force disabling of colors')
+  .option('--colors', 'force enabling of colors')
+  .option('--no-colors', 'force disabling of colors')
   .option('-G, --growl', 'enable growl notification support')
   .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
   .option('-R, --reporter <name>', 'specify the reporter to use')
@@ -84,18 +84,19 @@ program.command('run [suite] [test]')
 
   .action(require('../lib/command/run'));
 
-program.command('run:multiple [suite] [test]')
+program.command('run-multiple [suites...]')
   .description('Executes tests multiple')
+  .option('-c, --config [file]', 'configuration file to be used')
+  .option('--all', 'run all suites')
+  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
+  .option('-f, --fgrep <string>', 'only run tests containing <string>')
   .option('--steps', 'show step-by-step execution')
-  .option('--debug', 'output additional information')
   .option('--verbose', 'output internal logging information')
   .option('-o, --override [value]', 'override current config options')
   .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
   .option('-R, --reporter <name>', 'specify the reporter to use')
   .option('--recursive', 'include sub directories')
 
-  .option('--multiple <string>', 'run several instanses of codeceptjs to parallel test execution')
-
   .action(require('../lib/command/run-multiple'));
 
 if (process.argv.length <= 2) {
diff --git a/examples/codecept.json b/examples/codecept.json
index 78b8ed6b..14f6d83f 100644
--- a/examples/codecept.json
+++ b/examples/codecept.json
@@ -14,6 +14,12 @@
       "mochaFile": "./output/result.xml"
     }
   },
+  "multiple": {
+    "default": {
+      "grep": "signin",
+      "browsers": ["chrome", "firefox"]
+    }
+  },
   "name": "tests",
   "bootstrap": "./bootstrap.js",
   "include": {
diff --git a/lib/codecept.js b/lib/codecept.js
index b990c41b..0baf5aaf 100644
--- a/lib/codecept.js
+++ b/lib/codecept.js
@@ -34,7 +34,6 @@ class Codecept {
   init(dir, callback) {
     // preparing globals
     global.codecept_dir = dir;
-    global.suiteDetails = this.opts.child;
     global.output_dir = fsPath.resolve(dir, this.config.output);
     global.actor = global.codecept_actor = require('./actor');
     global.Helper = global.codecept_helper = require('./helper');
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index 5d6ac255..8c8a60b0 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -3,141 +3,126 @@ let getConfig = require('./utils').getConfig;
 let getTestRoot = require('./utils').getTestRoot;
 let deepMerge = require('./utils').deepMerge;
 let Codecept = require('../codecept');
-var fork = require('child_process').fork
+let fork = require('child_process').fork;
 let output = require('../output');
+const path = require('path');
+const runner = path.join(__dirname, '/../../bin/codecept');
+let config, childOpts = {};
+const copyOptions = ['steps', 'reporter', 'verbose', 'config', 'reporter-options', 'grep', 'fgrep'];
 
-module.exports = function (suite, test, options) {
+// codeceptjs run:multiple smoke:chrome regression:firefox - will launch smoke suite in chrome and regression in firefox
+// codeceptjs run:multiple smoke:chrome regression - will launch smoke suite in chrome and regression in firefox and chrome
+// codeceptjs run:multiple all - will launch all suites
+// codeceptjs run:multiple smoke regression'
+
+module.exports = function (suites, options) {
   // registering options globally to use in config
   process.profile = options.profile;
   let configFile = options.config;
   let codecept;
 
-  let testRoot = getTestRoot(suite);
-  let config = getConfig(testRoot, configFile);
+  let testRoot = getTestRoot(configFile);
+  config = getConfig(configFile);
+
+  let configMultiple = config.multiple;
 
-  // override config with options
-  if (options.override) {
-    config = deepMerge(config, JSON.parse(options.override));
+  if (!configMultiple) {
+    throw new Error(`Multiple suites not configured, add "multiple": { /../ } section to config`);
   }
 
-  let outputDir = config.output;
-  let defaultWindowSize = searchValueInObj('windowSize', config.helpers)
-
-  // run multiple codeceptjs with options
-  if (options.multiple) {
-    // get list of multiple options
-    let multipleOptions = options.multiple.replace(/ /g, "").split(';');
-    let browserList = ['chrome', 'firefox', 'ie', 'safari', 'edge'];
-    //iterate all multipleOptions
-    multipleOptions.forEach(function (multipleOption) {
-      // get suites
-      let browserSuites = multipleOption.split(':');
-      // if key in browser list, then seach suites for this browser
-      if (browserList.indexOf(browserSuites[0]) > -1) {
-        let browser = browserSuites[0];
-        let suites = browserSuites[1].split(',')
-        // iterate suites for browser
-        suites.forEach(function (suite) {
-          runnerHelper(browser, suite, config)
-        })
-      } else if (browserSuites[0] == 'all' && browserSuites.length == 1) {
-        let browsersConfig = Object.keys(config.multiple);
-        browsersConfig.forEach(function (browser) {
-          let suites = Object.keys(config.multiple[browser]);
-          suites.forEach(function (suite) {
-            runnerHelper(browser, suite, config)
-          })
-        });
-      } else if (browserSuites.length == 1 && browserSuites[0] != '') {
-        //run suite in all browsers
-        let suites = browserSuites[0].split(',');
-        suites.forEach(function (suite) {
-          let browserConfiguration = Object.keys(config.multiple)
-          browserConfiguration.forEach(function (browser) {
-            let configuration = config.multiple[browser];
-            if (configuration[suite]) {
-              runnerHelper(browser, suite, config)
-            }
-          })
-        })
-      } else throw new Error('Error while running codeceptjs multiply. Suites list:' + multipleOptions);
-    });
+  // copy opts to run
+  Object.keys(options)
+    .filter((key) => copyOptions.indexOf(key) > -1)
+    .forEach((key) => childOpts[key] = options[key]);
+
+  if (options.all) {
+    suites = Object.keys(configMultiple);
   }
 
+  //iterate options
+  suites.forEach(function (suite) {
 
-  function runnerHelper(browser, suite, config) {
-    let suiteConf = config.multiple[browser][suite];
-    let sizes = suiteConf.windowSizes || ['default']
-    // override browser
-    let overriddenConfig = replaceValue(new getConfig(testRoot, configFile), 'browser',
-      (browser === 'ie') ? 'internet explorer' : browser)
-
-    sizes.forEach(function (size) {
-      if (size != 'default') {
-        //override windowSize
-        overriddenConfig = replaceValue(overriddenConfig, 'windowSize', size)
-        overriddenConfig =
-          replaceValue(overriddenConfig, 'output', outputDir + browser + '_' + suite + '_' + size + '/')
-        overriddenConfig =
-          replaceValue(overriddenConfig, 'reportDir', outputDir + browser + '_' + suite + '_' + size + '/')
-      } else {
-        overriddenConfig = replaceValue(overriddenConfig, 'windowSize', defaultWindowSize)
-        overriddenConfig = replaceValue(overriddenConfig, 'output', outputDir + browser + '_' + suite + '/')
-        overriddenConfig = replaceValue(overriddenConfig, 'reportDir', outputDir + browser + '_' + suite + '/')
-      }
+    // get suites
+    suite = suite.split(':');
 
-      //override grep param and collect all params
-      let params = ['run', '--child', browser + '_' + suite + '_' + size, '--grep', suiteConf.grep,
-        '--override', JSON.stringify(overriddenConfig)];
+    let suiteName = suite[0];
+    let browser = suite[1];
+    let suiteConf = configMultiple[suiteName];
 
-      for (let i = 3; i < process.argv.length; i++) {
-        if (process.argv[i].indexOf('--multiple=') < 0)
-          if (process.argv[i].indexOf('--multiple') < 0) {
-            params.push(process.argv[i]);
-          } else i++
-      }
-      fork(__dirname.replace('/lib/command', '/bin/codecept'), params, {
-        stdio: [0, 1, 2, 'ipc']
-      }).on('exit', (code) => {
-        if (code != 0) process.exitCode = 1;
-      }).on('error', (err) => {
-        process.exitCode = 1;
-      });
-    })
+    if (!suiteConf) {
+      throw new Error(`Suite ${suiteName} was not configured in "multiple" section of config`);
+    }
+
+    runSuite(suiteName, suiteConf, browser);
+  });
+};
+
+function runSuite(suite, suiteConf, browser) {
+  let suiteBrowsers = suiteConf.browsers.map((b) => typeof b === 'object' ? b.browser : b);
+
+  if (!browser) {
+    // run all browsers in this suite
+    suiteBrowsers.forEach((b) => runSuite(suite, suiteConf, b));
+    return;
   }
 
-  function replaceValue(obj, key, value) {
-    /* search key in object recursive and replace value in it
-     */
-    if (obj instanceof Array) {
-      for (var i in obj) {
-        replaceValue(obj[i], key, value);
-      }
-    }
-    if (obj[key]) obj[key] = value;
-    if ((typeof obj == "object") && (obj !== null)) {
-      var children = Object.keys(obj);
-      if (children.length > 0) {
-        for (i = 0; i < children.length; i++) {
-          replaceValue(obj[children[i]], key, value);
-        }
-      }
+  let overriddenConfig = config;
+
+  if (suiteBrowsers.indexOf(browser) === -1) {
+    throw new Error(`Browser ${browser} not found in multiple suite "${suite}" config`);
+  }
+
+  let browserConfig = suiteConf.browsers
+    .map((b) => typeof b === 'object' ? b : { browser: b })
+    .filter((b) => b.browser == browser)[0];
+
+  for (let key in browserConfig) {
+    overriddenConfig.helpers = replaceValue(overriddenConfig.helpers, key, browserConfig[key]);
+  }
+
+  // tweaking default output directories and for mochawesome
+  overriddenConfig = replaceValue(overriddenConfig, 'output', `${config.output}/${suite}_${browser}/`);
+  overriddenConfig = replaceValue(overriddenConfig, 'reportDir', `${config.output}/${suite}_${browser}/`);
+
+  // override grep param and collect all params
+  let params = ['run',
+    '--child', `${suite}:${browser}`,
+    '--override', JSON.stringify(overriddenConfig)
+  ];
+
+  Object.keys(childOpts).forEach((key) => {
+    params.push('--' + key);
+    if (childOpts[key] !== true) params.push(childOpts[key]);
+  });
+
+  if (suiteConf.grep) {
+    params.push('--grep');
+    params.push(suiteConf.grep);
+  }
+
+  fork(runner, params, {stdio: [0, 1, 2, 'ipc']})
+    .on('exit', (code) => code === 0 ? null : process.exitCode = 1)
+    .on('error', (err) => process.exitCode = 1);
+}
+
+
+/**
+* search key in object recursive and replace value in it
+*/
+function replaceValue(obj, key, value) {
+  if (obj instanceof Array) {
+    for (var i in obj) {
+      replaceValue(obj[i], key, value);
     }
-    return obj;
-  };
-
-  function searchValueInObj(_for, _in) {
-    var r;
-    for (var p in _in) {
-      if (p === _for) {
-        return _in[p];
-      }
-      if (typeof _in[p] === 'object') {
-        if ((r = searchValueInObj(_for, _in[p])) !== null) {
-          return r;
-        }
+  }
+  if (obj[key]) obj[key] = value;
+  if (typeof obj === "object" && obj !== null) {
+    var children = Object.keys(obj);
+    if (children.length > 0) {
+      for (i = 0; i < children.length; i++) {
+        replaceValue(obj[children[i]], key, value);
       }
     }
-    return null;
   }
-};
+  return obj;
+}
diff --git a/lib/command/run.js b/lib/command/run.js
index dd6e3d7b..31f4a431 100644
--- a/lib/command/run.js
+++ b/lib/command/run.js
@@ -5,14 +5,14 @@ let deepMerge = require('./utils').deepMerge;
 let Codecept = require('../codecept');
 let output = require('../output');
 
-module.exports = function (suite, test, options) {
+module.exports = function (test, options) {
   // registering options globally to use in config
   process.profile = options.profile;
   let configFile = options.config;
   let codecept;
 
-  let testRoot = getTestRoot(suite);
-  let config = getConfig(testRoot, configFile);
+  let testRoot = getTestRoot(configFile);
+  let config = getConfig(configFile);
 
   // override config with options
   if (options.override) {
diff --git a/lib/command/utils.js b/lib/command/utils.js
index 7e6f6042..ccee2aa3 100644
--- a/lib/command/utils.js
+++ b/lib/command/utils.js
@@ -5,17 +5,9 @@ let output = require("../output");
 let fs = require('fs');
 let path = require('path');
 
-module.exports.getTestRoot = function (currentPath) {
-  let testsPath = path.resolve(currentPath || '.');
-  return testsPath;
-};
-
-module.exports.getConfig = function (testRoot, configFile) {
+module.exports.getConfig = function (configFile) {
 
-  // using relative config
-  if (configFile && !path.isAbsolute(configFile)) {
-    configFile = path.join(testRoot, configFile);
-  }
+  let testRoot = getTestRoot(configFile);
 
   let config,
     manualConfigFile = configFile && path.resolve(configFile),
@@ -63,9 +55,17 @@ function deepMerge(target, source) {
   }
   return target;
 }
-
 module.exports.deepMerge = deepMerge;
 
+
+function getTestRoot(currentPath) {
+  if (!currentPath) currentPath = '.';
+  if (!path.isAbsolute(currentPath)) currentPath = path.join(process.cwd(), currentPath);
+  return currentPath = !path.extname(currentPath) ? currentPath : path.dirname(currentPath);
+}
+
+module.exports.getTestRoot = getTestRoot;
+
 function configWithDefaults(config) {
   if (!config.include) config.include = {};
   if (!config.helpers) config.helpers = {};
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index b72a263e..e64425e1 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -313,10 +313,10 @@ class WebDriverIO extends Helper {
     withinStore.elsFn = this.browser.elements;
     this.context = locator;
     return this.browser.element(withStrictLocator(locator)).then((res) => {
-      this.browser.element = function(l) {
+      this.browser.element = function (l) {
         return this.elementIdElement(res.value.ELEMENT, l);
       };
-      this.browser.elements = function(l) {
+      this.browser.elements = function (l) {
         return this.elementIdElements(res.value.ELEMENT, l);
       };
     });
@@ -348,7 +348,7 @@ class WebDriverIO extends Helper {
    * ```
    */
   _locateCheckable(locator) {
-    return findCheckable(this.browser, locator).then(function(res) {
+    return findCheckable(this.browser, locator).then(function (res) {
       return res.value;
     });
   }
@@ -361,7 +361,7 @@ class WebDriverIO extends Helper {
    * ```
    */
   _locateClickable(locator) {
-    return findClickable(this.browser, locator).then(function(res) {
+    return findClickable(this.browser, locator).then(function (res) {
       return res.value;
     });
   }
@@ -374,7 +374,7 @@ class WebDriverIO extends Helper {
    * ```
    */
   _locateFields(locator) {
-    return findFields(this.browser, locator).then(function(res) {
+    return findFields(this.browser, locator).then(function (res) {
       return res.value;
     });
   }
@@ -419,7 +419,7 @@ class WebDriverIO extends Helper {
     if (context) {
       client = client.element(context);
     }
-    return findClickable(client, locator).then(function(res) {
+    return findClickable(client, locator).then(function (res) {
       if (!res.value || res.value.length === 0) {
         if (typeof locator === "object") locator = JSON.stringify(locator);
         throw new Error(`Clickable element ${locator.toString()} was not found by text|CSS|XPath`);
@@ -437,7 +437,7 @@ class WebDriverIO extends Helper {
     if (context) {
       client = client.element(context);
     }
-    return findClickable(client, locator).then(function(res) {
+    return findClickable(client, locator).then(function (res) {
       if (!res.value || res.value.length === 0) {
         if (typeof locator === "object") locator = JSON.stringify(locator);
         throw new Error(`Clickable element ${locator.toString()} was not found by text|CSS|XPath`);
@@ -458,7 +458,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/fillField }}
    */
   fillField(field, value) {
-    return findFields(this.browser, field).then(function(res) {
+    return findFields(this.browser, field).then(function (res) {
       if (!res.value || res.value.length === 0) {
         throw new Error(`Field ${field} not found by name|text|CSS|XPath`);
       }
@@ -471,7 +471,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/appendField }}
    */
   appendField(field, value) {
-    return findFields(this.browser, field).then(function(res) {
+    return findFields(this.browser, field).then(function (res) {
       if (!res.value || res.value.length === 0) {
         throw new Error(`Field ${field} not found by name|text|CSS|XPath`);
       }
@@ -485,7 +485,7 @@ class WebDriverIO extends Helper {
    *
    */
   selectOption(select, option) {
-    return findFields(this.browser, select).then(function(res) {
+    return findFields(this.browser, select).then(function (res) {
       if (!res.value || res.value.length === 0) {
         throw new Error(`Selectable field ${select} not found by name|text|CSS|XPath`);
       }
@@ -571,7 +571,7 @@ class WebDriverIO extends Helper {
         throw new Error(`Checkable ${field} cant be located by name|text|CSS|XPath`);
       }
       let elem = res.value[0];
-      return client.elementIdSelected(elem.ELEMENT).then(function(isSelected) {
+      return client.elementIdSelected(elem.ELEMENT).then(function (isSelected) {
         if (isSelected.value) return true;
         return this[clickMethod](elem.ELEMENT);
       });
@@ -582,7 +582,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/grabTextFrom }}
    */
   grabTextFrom(locator) {
-    return this.browser.getText(withStrictLocator(locator)).then(function(text) {
+    return this.browser.getText(withStrictLocator(locator)).then(function (text) {
       return text;
     });
   }
@@ -596,7 +596,7 @@ class WebDriverIO extends Helper {
    * ```
    */
   grabHTMLFrom(locator) {
-    return this.browser.getHTML(withStrictLocator(locator)).then(function(html) {
+    return this.browser.getHTML(withStrictLocator(locator)).then(function (html) {
       return html;
     });
   }
@@ -605,7 +605,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/grabValueFrom }}
    */
   grabValueFrom(locator) {
-    return this.browser.getValue(withStrictLocator(locator)).then(function(text) {
+    return this.browser.getValue(withStrictLocator(locator)).then(function (text) {
       return text;
     });
   }
@@ -614,7 +614,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/grabAttributeFrom }}
    */
   grabAttributeFrom(locator, attr) {
-    return this.browser.getAttribute(withStrictLocator(locator), attr).then(function(text) {
+    return this.browser.getAttribute(withStrictLocator(locator), attr).then(function (text) {
       return text;
     });
   }
@@ -693,7 +693,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/seeElement }}
    */
   seeElement(locator) {
-    return this.browser.isVisible(withStrictLocator(locator)).then(function(res) {
+    return this.browser.isVisible(withStrictLocator(locator)).then(function (res) {
       return truth(`elements of ${locator}`, 'to be seen').assert(res);
     });
   }
@@ -702,7 +702,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/dontSeeElement}}
    */
   dontSeeElement(locator) {
-    return this.browser.isVisible(withStrictLocator(locator)).then(function(res) {
+    return this.browser.isVisible(withStrictLocator(locator)).then(function (res) {
       return truth(`elements of ${locator}`, 'to be seen').negate(res);
     });
   }
@@ -711,7 +711,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/seeElementInDOM }}
    */
   seeElementInDOM(locator) {
-    return this.browser.elements(withStrictLocator(locator)).then(function(res) {
+    return this.browser.elements(withStrictLocator(locator)).then(function (res) {
       return empty('elements').negate(res.value);
     });
   }
@@ -720,7 +720,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/dontSeeElementInDOM }}
    */
   dontSeeElementInDOM(locator) {
-    return this.browser.elements(withStrictLocator(locator)).then(function(res) {
+    return this.browser.elements(withStrictLocator(locator)).then(function (res) {
       return empty('elements').assert(res.value);
     });
   }
@@ -753,7 +753,7 @@ class WebDriverIO extends Helper {
    */
   seeNumberOfElements(selector, num) {
     return this.browser.elements(withStrictLocator(selector))
-      .then(function(res) {
+      .then(function (res) {
         return assert.equal(res.value.length, num);
       });
   }
@@ -778,7 +778,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/seeInCurrentUrl }}
    */
   seeInCurrentUrl(url) {
-    return this.browser.url().then(function(res) {
+    return this.browser.url().then(function (res) {
       return stringIncludes('url').assert(url, res.value);
     });
   }
@@ -787,7 +787,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/dontSeeInCurrentUrl }}
    */
   dontSeeInCurrentUrl(url) {
-    return this.browser.url().then(function(res) {
+    return this.browser.url().then(function (res) {
       return stringIncludes('url').negate(url, res.value);
     });
   }
@@ -875,7 +875,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/clearField}}
    */
   clearField(field) {
-    return findFields(this.browser, field).then(function(res) {
+    return findFields(this.browser, field).then(function (res) {
       if (!res.value || res.value.length === 0) {
         throw new Error(`Field ${field} not found by name|text|CSS|XPath`);
       }
@@ -889,7 +889,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/seeCookie}}
    */
   seeCookie(name) {
-    return this.browser.getCookie(name).then(function(res) {
+    return this.browser.getCookie(name).then(function (res) {
       return truth('cookie ' + name, 'to be set').assert(res);
     });
   }
@@ -898,7 +898,7 @@ class WebDriverIO extends Helper {
    * {{> ../webapi/dontSeeCookie}}
    */
   dontSeeCookie(name) {
-    return this.browser.getCookie(name).then(function(res) {
+    return this.browser.getCookie(name).then(function (res) {
       return truth('cookie ' + name, 'to be set').negate(res);
     });
   }
@@ -915,7 +915,7 @@ class WebDriverIO extends Helper {
    * Don't confuse popups with modal windows, as created by [various libraries](http://jster.net/category/windows-modals-popups).
    */
   acceptPopup() {
-    return this.browser.alertText().then(function(res) {
+    return this.browser.alertText().then(function (res) {
       if (res !== null) {
         return this.alertAccept();
       }
@@ -926,7 +926,7 @@ class WebDriverIO extends Helper {
    * Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt.
    */
   cancelPopup() {
-    return this.browser.alertText().then(function(res) {
+    return this.browser.alertText().then(function (res) {
       if (res !== null) {
         return this.alertDismiss();
       }
@@ -937,7 +937,7 @@ class WebDriverIO extends Helper {
    * Checks that the active JavaScript popup, as created by `window.alert|window.confirm|window.prompt`, contains the given string.
    */
   seeInPopup(text) {
-    return this.browser.alertText().then(function(res) {
+    return this.browser.alertText().then(function (res) {
       if (res === null) {
         throw new Error('Popup is not opened');
       }
@@ -961,7 +961,7 @@ class WebDriverIO extends Helper {
     if (Array.isArray(key) && ~['Control', 'Command', 'Shift', 'Alt'].indexOf(key[0])) {
       modifier = key[0];
     }
-    return this.browser.keys(key).then(function() {
+    return this.browser.keys(key).then(function () {
       if (!modifier) return true;
       return this.keys(modifier); // release modifeier
     });
@@ -1023,14 +1023,14 @@ class WebDriverIO extends Helper {
   waitForText(text, sec, context) {
     sec = sec || this.options.waitForTimeout;
     context = context || 'body';
-    return this.browser.waitUntil(function() {
-        return this.getText(context).then(function(source) {
-          if (Array.isArray(source)) {
-            return source.filter(part => part.indexOf(text) >= 0).length > 0;
-          }
-          return source.indexOf(text) >= 0;
-        });
-      }, sec * 1000)
+    return this.browser.waitUntil(function () {
+      return this.getText(context).then(function (source) {
+        if (Array.isArray(source)) {
+          return source.filter(part => part.indexOf(text) >= 0).length > 0;
+        }
+        return source.indexOf(text) >= 0;
+      });
+    }, sec * 1000)
       .catch((e) => {
         if (e.type === 'WaitUntilTimeoutError') {
           return proceedSee.call(this, 'assert', text, context);
@@ -1101,7 +1101,7 @@ function proceedSee(assertType, text, context) {
   } else {
     description = 'element ' + context;
   }
-  return this.browser.getText(withStrictLocator(context)).then(function(source) {
+  return this.browser.getText(withStrictLocator(context)).then(function (source) {
     return stringIncludes(description)[assertType](text, source);
   });
 }
@@ -1118,7 +1118,7 @@ function findClickable(client, locator) {
     `.//a/img[normalize-space(@alt)=${literal}]/ancestor::a`,
     `.//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=${literal}]`
   ]);
-  return client.elements(narrowLocator).then(function(els) {
+  return client.elements(narrowLocator).then(function (els) {
     if (els.value.length) {
       return els;
     }
@@ -1130,7 +1130,7 @@ function findClickable(client, locator) {
       `.//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = ${literal}]`,
       `.//button[./@name = ${literal}]`
     ]);
-    return client.elements(wideLocator).then(function(els) {
+    return client.elements(wideLocator).then(function (els) {
       if (els.value.length) {
         return els;
       }
@@ -1159,7 +1159,7 @@ function findFields(client, locator) {
 }
 
 function proceedSeeField(assertType, field, value) {
-  return findFields(this.browser, field).then(function(res) {
+  return findFields(this.browser, field).then(function (res) {
     if (!res.value || res.value.length === 0) {
       throw new Error(`Field ${field} not found by name|text|CSS|XPath`);
     }
@@ -1206,7 +1206,7 @@ function proceedSeeField(assertType, field, value) {
 }
 
 function proceedSeeCheckbox(assertType, field) {
-  return findFields(this.browser, field).then(function(res) {
+  return findFields(this.browser, field).then(function (res) {
     if (!res.value || res.value.length === 0) {
       throw new Error(`Field ${field} not found by name|text|CSS|XPath`);
     }
@@ -1229,10 +1229,10 @@ function findCheckable(client, locator) {
     `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
     `.//label[contains(normalize-space(string(.)), ${literal})]//input[@type = 'radio' or @type = 'checkbox']`
   ]);
-  return client.elements(byText).then(function(els) {
+  return client.elements(byText).then(function (els) {
     if (els.value.length) return els;
     let byName = `.//input[@type = 'checkbox' or @type = 'radio'][@name = ${literal}]`;
-    return client.elements(byName).then(function(els) {
+    return client.elements(byName).then(function (els) {
       if (els.value.length) return els;
       return client.elements(locator); // by css or xpath
     });
@@ -1258,14 +1258,14 @@ function withStrictLocator(locator) {
   locator.toString = () => `{${key}: '${value}'}`;
 
   switch (key) {
-    case 'by':
-    case 'xpath':
-    case 'css':
-      return value;
-    case 'id':
-      return '#' + value;
-    case 'name':
-      return `[name="${value}"]`;
+  case 'by':
+  case 'xpath':
+  case 'css':
+    return value;
+  case 'id':
+    return '#' + value;
+  case 'name':
+    return `[name="${value}"]`;
   }
 }
 
diff --git a/lib/output.js b/lib/output.js
index 72c1feaa..6f6c5229 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,7 +1,6 @@
 'use strict';
 
 let colors = require('chalk');
-let print = console.log;
 let symbols = require('mocha/lib/reporters/base').symbols;
 
 let styles = {
@@ -14,6 +13,7 @@ let styles = {
 };
 
 let outputLevel = 0;
+let outputProcess = '';
 
 module.exports = {
   colors,
@@ -25,6 +25,11 @@ module.exports = {
     return outputLevel;
   },
 
+  process: (process) => {
+    if (process) outputProcess = `[${process}]`;
+    return outputProcess;
+  },
+
   debug: (msg) => {
     if (outputLevel >= 2) print(' '.repeat(this.stepShift), styles.debug("> " + msg));
   },
@@ -45,7 +50,7 @@ module.exports = {
     if (outputLevel === 0) return;
     if (!step) return;
     let sym = process.platform === 'win32' ? '*' : '•';
-    if (suiteDetails) process.stdout.write('[ ' + suiteDetails + ' ] ' )
+    if (suiteDetails) process.stdout.write('[ ' + suiteDetails + ' ] ');
     print(' '.repeat(this.stepShift), `${sym} ${step.toString()}`);
   },
 
@@ -83,21 +88,6 @@ module.exports = {
       print(' ' + colors.red.bold(`${symbols.err} FAILED`) + ' ' + colors.grey(`in ${test.duration}ms`));
       print();
     }
-
-
-  },
-
-  child: {
-    suite: (suite, options) => {
-      if (!suite.title) return;
-      process.stdout.write('[ ' + options + ' ] ' )
-    },
-    test: (test, options) => {
-      process.stdout.write('[ ' + options + ' ] ' )
-    },
-    results: (options) => {
-      process.stdout.write('[ ' + options + ' ] ' )
-    }
   },
 
   say: (message) => {
@@ -124,6 +114,12 @@ module.exports = {
   }
 };
 
+function print(...msg) {
+  if (outputProcess) {
+    msg.unshift(outputProcess);
+  }
+  console.log.apply(this, msg);
+}
 
 function ind() {
   return "  ";
diff --git a/lib/reporter/cli.js b/lib/reporter/cli.js
index e89050e3..6d41f8de 100644
--- a/lib/reporter/cli.js
+++ b/lib/reporter/cli.js
@@ -17,8 +17,9 @@ class Cli extends Base {
     if (opts.steps) level = 1;
     if (opts.debug) level = 2;
     if (opts.verbose) level = 3;
-    output.level(level);
 
+    output.process(opts.child);
+    output.level(level);
     output.print('CodeceptJS v' + require('../codecept').version());
     output.print(`Using test root "${global.codecept_dir}"`);
 
@@ -34,9 +35,6 @@ class Cli extends Base {
     });
 
     runner.on('suite', function (suite) {
-      if (global.suiteDetails) {
-        output.child.suite(suite, global.suiteDetails);
-      }
       output.suite.started(suite);
     });
 
@@ -45,17 +43,11 @@ class Cli extends Base {
         return output.scenario.failed(test);
       }
       cursor.CR();
-      if (global.suiteDetails) {
-        output.child.test(test, global.suiteDetails);
-      }
       output.test.failed(test);
     });
 
     runner.on('pending', function (test) {
       cursor.CR();
-      if (global.suiteDetails) {
-        output.child.test(test, global.suiteDetails);
-      }
       output.test.skipped(test);
     });
 
@@ -64,18 +56,12 @@ class Cli extends Base {
         return output.scenario.passed(test);
       }
       cursor.CR();
-      if (global.suiteDetails) {
-        output.child.test(test, global.suiteDetails);
-      }
       output.test.passed(test);
     });
 
     if (showSteps) {
       runner.on('test', function (test) {
         if (test.steps) {
-          if (global.suiteDetails) {
-            output.child.test(test, global.suiteDetails);
-          }
           output.test.started(test);
         }
       });
@@ -92,10 +78,6 @@ class Cli extends Base {
     let stats = this.stats;
     console.log();
 
-    if (global.suiteDetails) {
-      output.child.results(global.suiteDetails);
-    }
-
     // passes
     if (stats.failures) {
       output.print('-- FAILURES:');
diff --git a/lib/utils.js b/lib/utils.js
index 3660d700..c7f779d7 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -2,7 +2,7 @@ var fs = require('fs');
 var path = require('path');
 var getParameterNames = require('get-parameter-names');
 
-module.exports.fileExists = function(filePath) {
+module.exports.fileExists = function (filePath) {
   try {
     fs.statSync(filePath);
   } catch (err) {
@@ -11,7 +11,7 @@ module.exports.fileExists = function(filePath) {
   return true;
 };
 
-module.exports.isFile = function(filePath) {
+module.exports.isFile = function (filePath) {
   var filestat;
   try {
     filestat = fs.statSync(filePath);
@@ -22,16 +22,16 @@ module.exports.isFile = function(filePath) {
   return filestat.isFile();
 };
 
-module.exports.getParamNames = function(fn) {
+module.exports.getParamNames = function (fn) {
   if (fn.isSinonProxy) return [];
   return getParameterNames(fn);
 };
 
-module.exports.installedLocally = function() {
+module.exports.installedLocally = function () {
   return path.resolve(__dirname + '/../').indexOf(process.cwd()) === 0;
 };
 
-module.exports.methodsOfObject = function(obj, className) {
+module.exports.methodsOfObject = function (obj, className) {
   var methods = [];
 
   const standard = [
@@ -58,24 +58,24 @@ module.exports.methodsOfObject = function(obj, className) {
   return methods;
 };
 
-module.exports.template = function(template, data) {
-  return template.replace(/{{([^{}]*)}}/g, function(a, b) {
+module.exports.template = function (template, data) {
+  return template.replace(/{{([^{}]*)}}/g, function (a, b) {
     var r = data[b];
     if (r === undefined) return '';
     return r.toString();
   });
 };
 
-module.exports.ucfirst = function(str) {
+module.exports.ucfirst = function (str) {
   return str.charAt(0).toUpperCase() + str.substr(1);
 };
 
-module.exports.lcfirst = function(str) {
+module.exports.lcfirst = function (str) {
   return str.charAt(0).toLowerCase() + str.substr(1);
 };
 
 
-module.exports.hashCode = function(str) {
+module.exports.hashCode = function (str) {
   /* Getting 32bit hashCode of string like in Java.
    *  Used in Screenshot name to provide short screenshot names
    */
@@ -93,7 +93,7 @@ module.exports.hashCode = function(str) {
 module.exports.xpathLocator = {
   literal: (string) => {
     if (string.indexOf("'") > -1) {
-      string = string.split("'", -1).map(function(substr) {
+      string = string.split("'", -1).map(function (substr) {
         return "'" + substr + "'";
       }).join(',"\'",');
       return "concat(" + string + ")";
@@ -108,8 +108,8 @@ module.exports.xpathLocator = {
 
 module.exports.test = {
 
-  submittedData: function(dataFile) {
-    return function(key) {
+  submittedData: function (dataFile) {
+    return function (key) {
       var data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
       if (key) {
         return data.form[key];
@@ -118,7 +118,7 @@ module.exports.test = {
     };
   },
 
-  expectError: function() {
+  expectError: function () {
     throw new Error('should not be thrown');
   }
 };


Test patch:
diff --git a/test/data/fake_driver.js b/test/data/fake_driver.js
new file mode 100644
index 00000000..d4dc6f47
--- /dev/null
+++ b/test/data/fake_driver.js
@@ -0,0 +1,15 @@
+'use strict';
+let Helper = require('../../lib/helper');
+let output = require('../../lib/output');
+
+let browser;
+
+class FakeDriver extends Helper {
+
+  printBrowser() {
+    this.debug(this.config.browser);
+  }
+
+}
+
+module.exports = FakeDriver;
\ No newline at end of file
diff --git a/test/data/sandbox/browser_test.multiple.js b/test/data/sandbox/browser_test.multiple.js
new file mode 100644
index 00000000..19087cc1
--- /dev/null
+++ b/test/data/sandbox/browser_test.multiple.js
@@ -0,0 +1,5 @@
+Feature('print browser');
+
+Scenario('print browser info', (I) => {
+  I.printBrowser();
+});
\ No newline at end of file
diff --git a/test/data/sandbox/codecept.multiple.json b/test/data/sandbox/codecept.multiple.json
new file mode 100644
index 00000000..cbf7d079
--- /dev/null
+++ b/test/data/sandbox/codecept.multiple.json
@@ -0,0 +1,23 @@
+{
+  "tests": "./*_test.multiple.js",
+  "timeout": 10000,
+  "output": "./output",
+  "helpers": {
+    "FakeDriver": {
+      "require": "../fake_driver",
+      "browser": "dummy"
+    }
+  },
+  "multiple": {
+    "default": {
+      "browsers": ["chrome", { "browser": "firefox"}]
+    },
+    "mobile": {
+      "browsers": ["android", "safari"]
+    }
+  },
+  "include": {},
+  "bootstrap": false,
+  "mocha": {},
+  "name": "sandbox"
+}
\ No newline at end of file
diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js
index dd64cfb1..b8eb398b 100644
--- a/test/runner/codecept_test.js
+++ b/test/runner/codecept_test.js
@@ -5,9 +5,9 @@ let path = require('path');
 const exec = require('child_process').exec;
 let runner = path.join(__dirname, '/../../bin/codecept.js');
 let codecept_dir = path.join(__dirname, '/../data/sandbox')
-let codecept_run = runner +' run '+codecept_dir + ' ';
-let config_run_config = (config) => `${codecept_run} --config ${config}`;
-let config_run_override = (config) => `${codecept_run} --override '${JSON.stringify(config)}'`;
+let codecept_run = runner +' run';
+let codecept_run_config = (config) => `${codecept_run} --config ${codecept_dir}/${config}`;
+let config_run_override = (config) => `${codecept_run} --config ${codecept_dir} --override '${JSON.stringify(config)}'`;
 let fs;
 
 describe('CodeceptJS Runner', () => {
@@ -16,7 +16,8 @@ describe('CodeceptJS Runner', () => {
     global.codecept_dir = path.join(__dirname, '/../data/sandbox');
   });
 
-  it('should be executed', (done) => {
+  it('should be executed in current dir', (done) => {
+    process.chdir(codecept_dir);
     exec(codecept_run, (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       stdout.should.include('check current dir'); // test name
@@ -25,8 +26,19 @@ describe('CodeceptJS Runner', () => {
     });
   });
 
+
+
+  it('should be executed with config path', (done) => {
+    exec(codecept_run + ' -c '+codecept_dir, (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('check current dir'); // test name
+      assert(!err);
+      done();
+    });
+  });
+
   it('should show failures and exit with 1 on fail', (done) => {
-    exec(config_run_config('codecept.failed.json'), (err, stdout, stderr) => {
+    exec(codecept_run_config('codecept.failed.json'), (err, stdout, stderr) => {
       stdout.should.include('Not-A-Filesystem');
       stdout.should.include('file is not in dir');
       stdout.should.include('FAILURES');
@@ -36,7 +48,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run bootstrap', (done) => {
-    exec(config_run_config('codecept.bootstrap.sync.json'), (err, stdout, stderr) => {
+    exec(codecept_run_config('codecept.bootstrap.sync.json'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       stdout.should.include('I am bootstrap');
       assert(!err);
@@ -64,7 +76,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run hooks', (done) => {
-    exec(config_run_config('codecept.hooks.js'), (err, stdout, stderr) => {
+    exec(codecept_run_config('codecept.hooks.js'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       stdout.should.include('I am bootstrap');
       stdout.should.include('I am function hook');
@@ -74,7 +86,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run bootstrap/teardown as object', (done) => {
-    exec(config_run_config('codecept.bootstrap.obj.json'), (err, stdout, stderr) => {
+    exec(codecept_run_config('codecept.bootstrap.obj.json'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       stdout.should.include('I am bootstrap');
       stdout.should.include('I am teardown');
@@ -84,7 +96,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run dynamic config', (done) => {
-    exec(config_run_config('config.js'), (err, stdout, stderr) => {
+    exec(codecept_run_config('config.js'), (err, stdout, stderr) => {
       stdout.should.include('Filesystem'); // feature
       assert(!err);
       done();
@@ -92,7 +104,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run dynamic config with profile', (done) => {
-    exec(config_run_config('config.js') + ' --profile failed', (err, stdout, stderr) => {
+    exec(codecept_run_config('config.js') + ' --profile failed', (err, stdout, stderr) => {
       stdout.should.include('FAILURES');
       stdout.should.not.include('I am bootstrap');
       assert(err.code);
@@ -101,7 +113,7 @@ describe('CodeceptJS Runner', () => {
   });
 
   it('should run dynamic config with profile 2', (done) => {
-    exec(config_run_config('config.js') + ' --profile bootstrap', (err, stdout, stderr) => {
+    exec(codecept_run_config('config.js') + ' --profile bootstrap', (err, stdout, stderr) => {
       stdout.should.not.include('FAILURES'); // feature
       stdout.should.include('I am bootstrap');
       assert(!err);
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 8d2aaa4d..55310432 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -5,8 +5,8 @@ let path = require('path');
 const exec = require('child_process').exec;
 let runner = path.join(__dirname, '/../../bin/codecept.js');
 let codecept_dir = path.join(__dirname, '/../data/sandbox')
-let codecept_run = runner +' run '+codecept_dir + ' ';
-let config_run_config = (config) => `${codecept_run} --config ${config}`;
+let codecept_run = runner +' run';
+let config_run_config = (config) => `${codecept_run} --config ${codecept_dir}/${config}`;
 let config_run_override = (config) => `${codecept_run} --override '${JSON.stringify(config)}'`;
 let fs;
 
diff --git a/test/runner/run_multiple_test.js b/test/runner/run_multiple_test.js
new file mode 100644
index 00000000..27afc857
--- /dev/null
+++ b/test/runner/run_multiple_test.js
@@ -0,0 +1,77 @@
+'use strict';
+let should = require('chai').should();
+let assert = require('assert');
+let path = require('path');
+const exec = require('child_process').exec;
+let runner = path.join(__dirname, '/../../bin/codecept.js');
+let codecept_dir = path.join(__dirname, '/../data/sandbox')
+let codecept_run = runner +' run-multiple --config '+codecept_dir + '/codecept.multiple.json ';
+let fs;
+
+describe('CodeceptJS Multiple Runner', function() {
+
+  this.timeout(40000);
+
+  before(() => {
+    global.codecept_dir = path.join(__dirname, '/../data/sandbox');
+  });
+
+  it('should execute one suite with browser', (done) => {
+    exec(codecept_run+'default:firefox', (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('[default:firefox] print browser ');
+      stdout.should.not.include('[default:chrome] print browser ');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should execute one suite with browser', (done) => {
+    exec(codecept_run+'default:firefox', (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('[default:firefox] print browser ');
+      stdout.should.not.include('[default:chrome] print browser ');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should execute all suites', (done) => {
+    exec(codecept_run+'--all', (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('[default:firefox] print browser ');
+      stdout.should.include('[default:chrome] print browser ');
+      stdout.should.include('[mobile:safari] print browser ');
+      stdout.should.include('[mobile:android] print browser ');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should execute mutiple suites', (done) => {
+    exec(codecept_run+'default mobile', (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('[default:firefox] print browser ');
+      stdout.should.include('[default:chrome] print browser ');
+      stdout.should.include('[mobile:safari] print browser ');
+      stdout.should.include('[mobile:android] print browser ');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should print steps', (done) => {
+    exec(codecept_run+'default --steps', (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('[default:firefox] print browser ');
+      stdout.should.include('[default:firefox]  • I print browser');
+      stdout.should.include('[default:chrome] print browser ');
+      stdout.should.include('[default:chrome]  • I print browser');
+      assert(!err);
+      done();
+    });
+  });
+
+
+
+});
\ No newline at end of file