diff --git a/bin/codecept.js b/bin/codecept.js index de61613e9..2901f8c2e 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 ', 'reporter-specific options') .option('-R, --reporter ', '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 ', 'only run tests matching ') + .option('-f, --fgrep ', 'only run tests containing ') .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 ', 'reporter-specific options') .option('-R, --reporter ', 'specify the reporter to use') .option('--recursive', 'include sub directories') - .option('--multiple ', '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 78b8ed6b9..14f6d83f5 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 b990c41b2..0baf5aaf6 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 5d6ac255a..8c8a60b04 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 dd6e3d7b6..31f4a4313 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 7e6f60421..ccee2aa31 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 b72a263eb..e64425e11 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 72c1feaa4..6f6c52291 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 e89050e37..6d41f8de7 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 3660d7009..c7f779d7e 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'); } }; diff --git a/test/data/fake_driver.js b/test/data/fake_driver.js new file mode 100644 index 000000000..d4dc6f471 --- /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 000000000..19087cc17 --- /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 000000000..cbf7d079d --- /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 dd64cfb12..b8eb398b0 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 8d2aaa4dd..553104328 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 000000000..27afc857b --- /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