From 95753e6edc304818309ccf8cf7f9753823a5d08e Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 05:15:35 +0200 Subject: [PATCH 01/13] refactored mocha, implemented hooks --- lib/cli.js | 257 -------------------------- lib/codecept.js | 2 +- lib/command/gherkin/snippets.js | 2 +- lib/command/gherkin/steps.js | 2 +- lib/container.js | 2 +- lib/event.js | 1 + lib/interfaces/bdd.js | 81 -------- lib/interfaces/featureConfig.js | 69 ------- lib/interfaces/gherkin.js | 201 -------------------- lib/interfaces/inject.js | 24 --- lib/interfaces/scenarioConfig.js | 114 ------------ lib/listener/steps.js | 118 ++++++------ lib/mochaFactory.js | 113 ------------ lib/output.js | 16 +- lib/scenario.js | 203 -------------------- lib/ui.js | 236 ------------------------ lib/workers.js | 2 +- test/unit/bdd_test.js | 4 +- test/unit/data/ui_test.js | 10 +- test/unit/scenario_test.js | 130 ++++++------- test/unit/ui_test.js | 306 +++++++++++++++---------------- 21 files changed, 296 insertions(+), 1597 deletions(-) delete mode 100644 lib/cli.js delete mode 100644 lib/interfaces/bdd.js delete mode 100644 lib/interfaces/featureConfig.js delete mode 100644 lib/interfaces/gherkin.js delete mode 100644 lib/interfaces/inject.js delete mode 100644 lib/interfaces/scenarioConfig.js delete mode 100644 lib/mochaFactory.js delete mode 100644 lib/scenario.js delete mode 100644 lib/ui.js diff --git a/lib/cli.js b/lib/cli.js deleted file mode 100644 index a408fe1c1..000000000 --- a/lib/cli.js +++ /dev/null @@ -1,257 +0,0 @@ -const { - reporters: { Base }, -} = require('mocha'); -const ms = require('ms'); -const event = require('./event'); -const AssertionFailedError = require('./assert/error'); -const output = require('./output'); - -const cursor = Base.cursor; -let currentMetaStep = []; -let codeceptjsEventDispatchersRegistered = false; - -class Cli extends Base { - constructor(runner, opts) { - super(runner); - let level = 0; - this.loadedTests = []; - opts = opts.reporterOptions || opts; - if (opts.steps) level = 1; - if (opts.debug) level = 2; - if (opts.verbose) level = 3; - output.level(level); - output.print(`CodeceptJS v${require('./codecept').version()} ${output.standWithUkraine()}`); - output.print(`Using test root "${global.codecept_dir}"`); - - const showSteps = level >= 1; - - if (level >= 2) { - const Containter = require('./container'); - output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)); - output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)); - } - - runner.on('start', () => { - console.log(); - }); - - runner.on('suite', suite => { - output.suite.started(suite); - }); - - runner.on('fail', test => { - if (test.ctx.currentTest) { - this.loadedTests.push(test.ctx.currentTest.uid); - } - if (showSteps && test.steps) { - return output.scenario.failed(test); - } - cursor.CR(); - output.test.failed(test); - }); - - runner.on('pending', test => { - if (test.parent && test.parent.pending) { - const suite = test.parent; - const skipInfo = suite.opts.skipInfo || {}; - skipTestConfig(test, skipInfo.message); - } else { - skipTestConfig(test, null); - } - this.loadedTests.push(test.uid); - cursor.CR(); - output.test.skipped(test); - }); - - runner.on('pass', test => { - if (showSteps && test.steps) { - return output.scenario.passed(test); - } - cursor.CR(); - output.test.passed(test); - }); - - if (showSteps) { - runner.on('test', test => { - currentMetaStep = []; - if (test.steps) { - output.test.started(test); - } - }); - - if (!codeceptjsEventDispatchersRegistered) { - codeceptjsEventDispatchersRegistered = true; - - event.dispatcher.on(event.bddStep.started, step => { - output.stepShift = 2; - output.step(step); - }); - - event.dispatcher.on(event.step.started, step => { - let processingStep = step; - const metaSteps = []; - while (processingStep.metaStep) { - metaSteps.unshift(processingStep.metaStep); - processingStep = processingStep.metaStep; - } - const shift = metaSteps.length; - - for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) { - if (currentMetaStep[i] !== metaSteps[i]) { - output.stepShift = 3 + 2 * i; - if (!metaSteps[i]) continue; - // bdd steps are handled by bddStep.started - if (metaSteps[i].isBDD()) continue; - output.step(metaSteps[i]); - } - } - currentMetaStep = metaSteps; - output.stepShift = 3 + 2 * shift; - if (step.helper.constructor.name !== 'ExpectHelper') { - output.step(step); - } - }); - - event.dispatcher.on(event.step.finished, () => { - output.stepShift = 0; - }); - } - } - - runner.on('suite end', suite => { - let skippedCount = 0; - const grep = runner._grep; - for (const test of suite.tests) { - if (!test.state && !this.loadedTests.includes(test.uid)) { - if (matchTest(grep, test.title)) { - if (!test.opts) { - test.opts = {}; - } - if (!test.opts.skipInfo) { - test.opts.skipInfo = {}; - } - skipTestConfig(test, "Skipped due to failure in 'before' hook"); - output.test.skipped(test); - skippedCount += 1; - } - } - } - - this.stats.pending += skippedCount; - this.stats.tests += skippedCount; - }); - - runner.on('end', this.result.bind(this)); - } - - result() { - const stats = this.stats; - stats.failedHooks = 0; - console.log(); - - // passes - if (stats.failures) { - output.print(output.styles.bold('-- FAILURES:')); - } - - const failuresLog = []; - - // failures - if (stats.failures) { - // append step traces - this.failures.map(test => { - const err = test.err; - - let log = ''; - - if (err instanceof AssertionFailedError) { - err.message = err.inspect(); - } - - const steps = test.steps || (test.ctx && test.ctx.test.steps); - - if (steps && steps.length) { - let scenarioTrace = ''; - steps.reverse().forEach(step => { - const line = `- ${step.toCode()} ${step.line()}`; - // if (step.status === 'failed') line = '' + line; - scenarioTrace += `\n${line}`; - }); - log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; - } - - // display artifacts in debug mode - if (test?.artifacts && Object.keys(test.artifacts).length) { - log += `\n${output.styles.bold('Artifacts:')}`; - for (const artifact of Object.keys(test.artifacts)) { - log += `\n- ${artifact}: ${test.artifacts[artifact]}`; - } - } - - try { - let stack = err.stack ? err.stack.split('\n') : []; - if (stack[0] && stack[0].includes(err.message)) { - stack.shift(); - } - - if (output.level() < 3) { - stack = stack.slice(0, 3); - } - - err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; - - // clone err object so stack trace adjustments won't affect test other reports - test.err = err; - return test; - } catch (e) { - throw Error(e); - } - }); - - const originalLog = Base.consoleLog; - Base.consoleLog = (...data) => { - failuresLog.push([...data]); - originalLog(...data); - }; - Base.list(this.failures); - Base.consoleLog = originalLog; - console.log(); - } - - this.failures.forEach(failure => { - if (failure.constructor.name === 'Hook') { - stats.failedHooks += 1; - } - }); - event.emit(event.all.failures, { failuresLog, stats }); - output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks); - - if (stats.failures && output.level() < 3) { - output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); - } - } -} - -function matchTest(grep, test) { - if (grep) { - return grep.test(test); - } - return true; -} - -function skipTestConfig(test, message) { - if (!test.opts) { - test.opts = {}; - } - if (!test.opts.skipInfo) { - test.opts.skipInfo = {}; - } - test.opts.skipInfo.message = test.opts.skipInfo.message || message; - test.opts.skipInfo.isFastSkipped = true; - event.emit(event.test.skipped, test); - test.state = 'skipped'; -} - -module.exports = function (runner, opts) { - return new Cli(runner, opts); -}; diff --git a/lib/codecept.js b/lib/codecept.js index 1367f5e48..2a6d00e10 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -84,7 +84,7 @@ class Codecept { global.codeceptjs = require('./index'); // load all objects // BDD - const stepDefinitions = require('./interfaces/bdd'); + const stepDefinitions = require('./mocha/bdd'); global.Given = stepDefinitions.Given; global.When = stepDefinitions.When; global.Then = stepDefinitions.Then; diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js index e34a62e96..fded183cb 100644 --- a/lib/command/gherkin/snippets.js +++ b/lib/command/gherkin/snippets.js @@ -8,7 +8,7 @@ const fsPath = require('path'); const { getConfig, getTestRoot } = require('../utils'); const Codecept = require('../../codecept'); const output = require('../../output'); -const { matchStep } = require('../../interfaces/bdd'); +const { matchStep } = require('../../mocha/bdd'); const uuidFn = Messages.IdGenerator.uuid(); const builder = new Gherkin.AstBuilder(uuidFn); diff --git a/lib/command/gherkin/steps.js b/lib/command/gherkin/steps.js index ee149eb5b..a100ed6a7 100644 --- a/lib/command/gherkin/steps.js +++ b/lib/command/gherkin/steps.js @@ -1,7 +1,7 @@ const { getConfig, getTestRoot } = require('../utils'); const Codecept = require('../../codecept'); const output = require('../../output'); -const { getSteps } = require('../../interfaces/bdd'); +const { getSteps } = require('../../mocha/bdd'); module.exports = function (genPath, options) { const configFile = options.config || genPath; diff --git a/lib/container.js b/lib/container.js index 34a0058de..186f12b53 100644 --- a/lib/container.js +++ b/lib/container.js @@ -3,7 +3,7 @@ const path = require('path'); const { MetaStep } = require('./step'); const { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally } = require('./utils'); const Translation = require('./translation'); -const MochaFactory = require('./mochaFactory'); +const MochaFactory = require('./mocha/factory'); const recorder = require('./recorder'); const event = require('./event'); const WorkerStorage = require('./workerStorage'); diff --git a/lib/event.js b/lib/event.js index 676354be7..d7cc05046 100644 --- a/lib/event.js +++ b/lib/event.js @@ -60,6 +60,7 @@ module.exports = { passed: 'hook.passed', failed: 'hook.failed', }, + /** * @type {object} * @constant diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js deleted file mode 100644 index 642cd2591..000000000 --- a/lib/interfaces/bdd.js +++ /dev/null @@ -1,81 +0,0 @@ -const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('@cucumber/cucumber-expressions') -const Config = require('../config') - -let steps = {} - -const STACK_POSITION = 2 - -/** - * @param {*} step - * @param {*} fn - */ -const addStep = (step, fn) => { - const avoidDuplicateSteps = Config.get('gherkin', {}).avoidDuplicateSteps || false - const stack = new Error().stack - if (avoidDuplicateSteps && steps[step]) { - throw new Error(`Step '${step}' is already defined`) - } - steps[step] = fn - fn.line = stack && stack.split('\n')[STACK_POSITION] - if (fn.line) { - fn.line = fn.line - .trim() - .replace(/^at (.*?)\(/, '(') - .replace(codecept_dir, '.') - } -} - -const parameterTypeRegistry = new ParameterTypeRegistry() - -const matchStep = (step) => { - for (const stepName in steps) { - if (stepName.indexOf('/') === 0) { - const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || [] - const res = step.match(new RegExp(regExpArr[1], regExpArr[2])) - if (res) { - const fn = steps[stepName] - fn.params = res.slice(1) - return fn - } - continue - } - const expression = new CucumberExpression(stepName, parameterTypeRegistry) - const res = expression.match(step) - if (res) { - const fn = steps[stepName] - fn.params = res.map((arg) => arg.getValue()) - return fn - } - } - throw new Error(`No steps matching "${step.toString()}"`) -} - -const clearSteps = () => { - steps = {} -} - -const getSteps = () => { - return steps -} - -const defineParameterType = (options) => { - const parameterType = buildParameterType(options) - parameterTypeRegistry.defineParameterType(parameterType) -} - -const buildParameterType = ({ name, regexp, transformer, useForSnippets, preferForRegexpMatch }) => { - if (typeof useForSnippets !== 'boolean') useForSnippets = true - if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false - return new ParameterType(name, regexp, null, transformer, useForSnippets, preferForRegexpMatch) -} - -module.exports = { - Given: addStep, - When: addStep, - Then: addStep, - And: addStep, - matchStep, - getSteps, - clearSteps, - defineParameterType, -} diff --git a/lib/interfaces/featureConfig.js b/lib/interfaces/featureConfig.js deleted file mode 100644 index ea9853197..000000000 --- a/lib/interfaces/featureConfig.js +++ /dev/null @@ -1,69 +0,0 @@ -/** @class */ -class FeatureConfig { - constructor(suite) { - this.suite = suite - } - - /** - * Retry this suite for x times - * - * @param {number} retries - * @returns {this} - */ - retry(retries) { - this.suite.retries(retries) - return this - } - - /** - * Set timeout for this suite - * @param {number} timeout - * @returns {this} - * @deprecated - */ - timeout(timeout) { - console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`) - console.log(`Please use Feature('${this.suite.title}', { timeout: ${timeout / 1000} }) instead`) - console.log('Timeout should be set in seconds') - this.suite.timeout(timeout) - return this - } - - /** - * Configures a helper. - * Helper name can be omitted and values will be applied to first helper. - * @param {string | Object} helper - * @param {Object} [obj] - * @returns {this} - */ - config(helper, obj) { - if (!obj) { - obj = helper - helper = 0 - } - if (typeof obj === 'function') { - obj = obj(this.suite) - } - if (!this.suite.config) { - this.suite.config = {} - } - this.suite.config[helper] = obj - return this - } - - /** - * Append a tag name to scenario title - * @param {string} tagName - * @returns {this} - */ - tag(tagName) { - if (tagName[0] !== '@') { - tagName = `@${tagName}` - } - this.suite.tags.push(tagName) - this.suite.title = `${this.suite.title.trim()} ${tagName}` - return this - } -} - -module.exports = FeatureConfig diff --git a/lib/interfaces/gherkin.js b/lib/interfaces/gherkin.js deleted file mode 100644 index e8a7a9853..000000000 --- a/lib/interfaces/gherkin.js +++ /dev/null @@ -1,201 +0,0 @@ -const Gherkin = require('@cucumber/gherkin') -const Messages = require('@cucumber/messages') -const { Context, Suite, Test } = require('mocha') -const debug = require('debug')('codeceptjs:bdd') - -const { matchStep } = require('./bdd') -const event = require('../event') -const scenario = require('../scenario') -const Step = require('../step') -const DataTableArgument = require('../data/dataTableArgument') -const transform = require('../transform') - -const uuidFn = Messages.IdGenerator.uuid() -const builder = new Gherkin.AstBuilder(uuidFn) -const matcher = new Gherkin.GherkinClassicTokenMatcher() -const parser = new Gherkin.Parser(builder, matcher) -parser.stopAtFirstError = false - -module.exports = (text, file) => { - const ast = parser.parse(text) - let currentLanguage - - if (ast.feature) { - currentLanguage = getTranslation(ast.feature.language) - } - - if (!ast.feature) { - throw new Error(`No 'Features' available in Gherkin '${file}' provided!`) - } - const suite = new Suite(ast.feature.name, new Context()) - const tags = ast.feature.tags.map((t) => t.name) - suite.title = `${suite.title} ${tags.join(' ')}`.trim() - suite.tags = tags || [] - suite.comment = ast.feature.description - suite.feature = ast.feature - suite.file = file - suite.timeout(0) - - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) - suite.afterEach('codeceptjs.after', () => scenario.teardown(suite)) - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) - suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)) - - const runSteps = async (steps) => { - for (const step of steps) { - const metaStep = new Step.MetaStep(null, step.text) - metaStep.actor = step.keyword.trim() - let helperStep - const setMetaStep = (step) => { - helperStep = step - if (step.metaStep) { - if (step.metaStep === metaStep) { - return - } - setMetaStep(step.metaStep) - return - } - step.metaStep = metaStep - } - const fn = matchStep(step.text) - - if (step.dataTable) { - fn.params.push({ - ...step.dataTable, - parse: () => new DataTableArgument(step.dataTable), - }) - metaStep.comment = `\n${transformTable(step.dataTable)}` - } - - if (step.docString) { - fn.params.push(step.docString) - metaStep.comment = `\n"""\n${step.docString.content}\n"""` - } - - step.startTime = Date.now() - step.match = fn.line - event.emit(event.bddStep.before, step) - event.emit(event.bddStep.started, metaStep) - event.dispatcher.prependListener(event.step.before, setMetaStep) - try { - debug(`Step '${step.text}' started...`) - await fn(...fn.params) - debug('Step passed') - step.status = 'passed' - } catch (err) { - debug(`Step failed: ${err?.message}`) - step.status = 'failed' - step.err = err - throw err - } finally { - step.endTime = Date.now() - event.dispatcher.removeListener(event.step.before, setMetaStep) - } - event.emit(event.bddStep.finished, metaStep) - event.emit(event.bddStep.after, step) - } - } - - for (const child of ast.feature.children) { - if (child.background) { - suite.beforeEach( - 'Before', - scenario.injected(async () => runSteps(child.background.steps), suite, 'before'), - ) - continue - } - if ( - child.scenario && - (currentLanguage - ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline - : child.scenario.keyword === 'Scenario Outline') - ) { - for (const examples of child.scenario.examples) { - const fields = examples.tableHeader.cells.map((c) => c.value) - for (const example of examples.tableBody) { - let exampleSteps = [...child.scenario.steps] - const current = {} - for (const index in example.cells) { - const placeholder = fields[index] - const value = transform('gherkin.examples', example.cells[index].value) - example.cells[index].value = value - current[placeholder] = value - exampleSteps = exampleSteps.map((step) => { - step = { ...step } - step.text = step.text.split(`<${placeholder}>`).join(value) - return step - }) - } - const tags = child.scenario.tags.map((t) => t.name).concat(examples.tags.map((t) => t.name)) - let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim() - - for (const [key, value] of Object.entries(current)) { - if (title.includes(`<${key}>`)) { - title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value) - } - } - - const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))) - test.tags = suite.tags.concat(tags) - test.file = file - suite.addTest(scenario.test(test)) - } - } - continue - } - - if (child.scenario) { - const tags = child.scenario.tags.map((t) => t.name) - const title = `${child.scenario.name} ${tags.join(' ')}`.trim() - const test = new Test(title, async () => runSteps(child.scenario.steps)) - test.tags = suite.tags.concat(tags) - test.file = file - suite.addTest(scenario.test(test)) - } - } - - return suite -} - -function transformTable(table) { - let str = '' - for (const id in table.rows) { - const cells = table.rows[id].cells - str += cells - .map((c) => c.value) - .map((c) => c.padEnd(15)) - .join(' | ') - str += '\n' - } - return str -} -function addExampleInTable(exampleSteps, placeholders) { - const steps = JSON.parse(JSON.stringify(exampleSteps)) - for (const placeholder in placeholders) { - steps.map((step) => { - step = { ...step } - if (step.dataTable) { - for (const id in step.dataTable.rows) { - const cells = step.dataTable.rows[id].cells - cells.map((c) => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder]))) - } - } - return step - }) - } - return steps -} - -function getTranslation(language) { - const translations = Object.keys(require('../../translations')) - - for (const availableTranslation of translations) { - if (!language) { - break - } - - if (availableTranslation.includes(language)) { - return require('../../translations')[availableTranslation] - } - } -} diff --git a/lib/interfaces/inject.js b/lib/interfaces/inject.js deleted file mode 100644 index 712020ba3..000000000 --- a/lib/interfaces/inject.js +++ /dev/null @@ -1,24 +0,0 @@ -const parser = require('../parser'); - -const getInjectedArguments = (fn, test) => { - const container = require('../container'); - const testArgs = {}; - const params = parser.getParams(fn) || []; - const objects = container.support(); - for (const key of params) { - testArgs[key] = {}; - if (test && test.inject && test.inject[key]) { - // @FIX: need fix got inject - testArgs[key] = test.inject[key]; - continue; - } - if (!objects[key]) { - throw new Error(`Object of type ${key} is not defined in container`); - } - testArgs[key] = container.support(key); - } - - return testArgs; -}; - -module.exports.getInjectedArguments = getInjectedArguments; diff --git a/lib/interfaces/scenarioConfig.js b/lib/interfaces/scenarioConfig.js deleted file mode 100644 index 4331fb0e6..000000000 --- a/lib/interfaces/scenarioConfig.js +++ /dev/null @@ -1,114 +0,0 @@ -/** @class */ -class ScenarioConfig { - constructor(test) { - this.test = test - } - - /** - * Declares that test throws error. - * Can pass an Error object or regex matching expected message. - * - * @param {*} err - * @returns {this} - */ - throws(err) { - this.test.throws = err - return this - } - - /** - * Declares that test should fail. - * If test passes - throws an error. - * Can pass an Error object or regex matching expected message. - * - * @returns {this} - */ - fails() { - this.test.throws = new Error() - return this - } - - /** - * Retry this test for x times - * - * @param {number} retries - * @returns {this} - */ - retry(retries) { - if (process.env.SCENARIO_ONLY) retries = -retries - this.test.retries(retries) - return this - } - - /** - * Set timeout for this test - * @param {number} timeout - * @returns {this} - */ - timeout(timeout) { - console.log(`Scenario('${this.test.title}', () => {}).timeout(${timeout}) is deprecated!`) - console.log(`Please use Scenario('${this.test.title}', { timeout: ${timeout / 1000} }, () => {}) instead`) - console.log('Timeout should be set in seconds') - - this.test.timeout(timeout) - return this - } - - /** - * Pass in additional objects to inject into test - * @param {Object} obj - * @returns {this} - */ - inject(obj) { - this.test.inject = obj - return this - } - - /** - * Configures a helper. - * Helper name can be omitted and values will be applied to first helper. - * @param {string | Object} helper - * @param {Object} [obj] - * @returns {this} - */ - async config(helper, obj) { - if (!obj) { - obj = helper - helper = 0 - } - if (typeof obj === 'function') { - obj = await obj(this.test) - } - if (!this.test.config) { - this.test.config = {} - } - this.test.config[helper] = obj - return this - } - - /** - * Append a tag name to scenario title - * @param {string} tagName - * @returns {this} - */ - tag(tagName) { - if (tagName[0] !== '@') tagName = `@${tagName}` - this.test.tags.push(tagName) - this.test.title = `${this.test.title.trim()} ${tagName}` - return this - } - - /** - * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection - * @param {Object} dependencies - * @returns {this} - */ - injectDependencies(dependencies) { - Object.keys(dependencies).forEach((key) => { - this.test.inject[key] = dependencies[key] - }) - return this - } -} - -module.exports = ScenarioConfig diff --git a/lib/listener/steps.js b/lib/listener/steps.js index ef5443a07..ea999cbec 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -1,83 +1,87 @@ -const debug = require('debug')('codeceptjs:steps') -const event = require('../event') -const store = require('../store') -const output = require('../output') +const debug = require('debug')('codeceptjs:steps'); +const event = require('../event'); +const store = require('../store'); +const output = require('../output'); +const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks'); -let currentTest -let currentHook +let currentTest; +let currentHook; /** * Register steps inside tests */ module.exports = function () { - event.dispatcher.on(event.test.before, (test) => { - test.startedAt = +new Date() - test.artifacts = {} - }) + event.dispatcher.on(event.test.before, test => { + test.startedAt = +new Date(); + test.artifacts = {}; + }); - event.dispatcher.on(event.test.started, (test) => { - currentTest = test - currentTest.steps = [] - if (!('retryNum' in currentTest)) currentTest.retryNum = 0 - else currentTest.retryNum += 1 - }) + event.dispatcher.on(event.test.started, test => { + currentTest = test; + currentTest.steps = []; + if (!('retryNum' in currentTest)) currentTest.retryNum = 0; + else currentTest.retryNum += 1; + }); - event.dispatcher.on(event.test.after, (test) => { - currentTest = null - }) + event.dispatcher.on(event.test.after, test => { + currentTest = null; + }); - event.dispatcher.on(event.test.finished, (test) => {}) + event.dispatcher.on(event.test.finished, test => {}); - event.dispatcher.on(event.hook.started, (suite) => { - currentHook = suite.ctx.test - currentHook.steps = [] + event.dispatcher.on(event.hook.started, hook => { + currentHook = hook.ctx.test; + currentHook.steps = []; - if (suite.ctx && suite.ctx.test) output.log(`--- STARTED ${suite.ctx.test.title} ---`) - }) + output.hook.started(hook); - event.dispatcher.on(event.hook.passed, (suite) => { - currentHook = null - if (suite.ctx && suite.ctx.test) output.log(`--- ENDED ${suite.ctx.test.title} ---`) - }) + if (hook.ctx && hook.ctx.test) output.log(`--- STARTED ${hook.ctx.test.title} ---`); + }); + + event.dispatcher.on(event.hook.passed, hook => { + currentHook = null; + output.hook.passed(hook); + if (hook.ctx && hook.ctx.test) output.log(`--- ENDED ${hook.ctx.test.title} ---`); + }); event.dispatcher.on(event.test.failed, () => { const cutSteps = function (current) { - const failureIndex = current.steps.findIndex((el) => el.status === 'failed') + const failureIndex = current.steps.findIndex(el => el.status === 'failed'); // To be sure that failed test will be failed in report - current.state = 'failed' - current.steps.length = failureIndex + 1 - return current - } + current.state = 'failed'; + current.steps.length = failureIndex + 1; + return current; + }; if (currentHook && Array.isArray(currentHook.steps) && currentHook.steps.length) { - currentHook = cutSteps(currentHook) - return (currentHook = null) + currentHook = cutSteps(currentHook); + return (currentHook = null); } - if (!currentTest) return + if (!currentTest) return; // last step is failing step - if (!currentTest.steps.length) return - return (currentTest = cutSteps(currentTest)) - }) + if (!currentTest.steps.length) return; + return (currentTest = cutSteps(currentTest)); + }); event.dispatcher.on(event.test.passed, () => { - if (!currentTest) return + if (!currentTest) return; // To be sure that passed test will be passed in report - delete currentTest.err - currentTest.state = 'passed' - }) + delete currentTest.err; + currentTest.state = 'passed'; + }); - event.dispatcher.on(event.step.started, (step) => { - step.startedAt = +new Date() - step.test = currentTest + event.dispatcher.on(event.step.started, step => { + step.startedAt = +new Date(); + step.test = currentTest; if (currentHook && Array.isArray(currentHook.steps)) { - return currentHook.steps.push(step) + return currentHook.steps.push(step); } - if (!currentTest || !currentTest.steps) return - currentTest.steps.push(step) - }) + if (!currentTest || !currentTest.steps) return; + currentTest.steps.push(step); + }); - event.dispatcher.on(event.step.finished, (step) => { - step.finishedAt = +new Date() - if (step.startedAt) step.duration = step.finishedAt - step.startedAt - debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`) - }) -} + event.dispatcher.on(event.step.finished, step => { + step.finishedAt = +new Date(); + if (step.startedAt) step.duration = step.finishedAt - step.startedAt; + debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`); + }); +}; diff --git a/lib/mochaFactory.js b/lib/mochaFactory.js deleted file mode 100644 index 43502f647..000000000 --- a/lib/mochaFactory.js +++ /dev/null @@ -1,113 +0,0 @@ -const Mocha = require('mocha'); -const fsPath = require('path'); -const fs = require('fs'); -const reporter = require('./cli'); -const gherkinParser = require('./interfaces/gherkin'); -const output = require('./output'); -const { genTestId } = require('./utils'); -const ConnectionRefused = require('./helper/errors/ConnectionRefused'); - -const scenarioUi = fsPath.join(__dirname, './ui.js'); - -let mocha; - -class MochaFactory { - static create(config, opts) { - mocha = new Mocha(Object.assign(config, opts)); - output.process(opts.child); - mocha.ui(scenarioUi); - - Mocha.Runner.prototype.uncaught = function (err) { - if (err) { - if (err.toString().indexOf('ECONNREFUSED') >= 0) { - err = new ConnectionRefused(err); - } - output.error(err); - output.print(err.stack); - process.exit(1); - } - output.error('Uncaught undefined exception'); - process.exit(1); - }; - - mocha.loadFiles = (fn) => { - // load features - if (mocha.suite.suites.length === 0) { - mocha.files - .filter(file => file.match(/\.feature$/)) - .forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file))); - - // remove feature files - mocha.files = mocha.files.filter(file => !file.match(/\.feature$/)); - - Mocha.prototype.loadFiles.call(mocha, fn); - - // add ids for each test and check uniqueness - const dupes = []; - let missingFeatureInFile = []; - const seenTests = []; - mocha.suite.eachTest(test => { - test.uid = genTestId(test); - - const name = test.fullTitle(); - if (seenTests.includes(test.uid)) { - dupes.push(name); - } - seenTests.push(test.uid); - - if (name.slice(0, name.indexOf(':')) === '') { - missingFeatureInFile.push(test.file); - } - }); - if (dupes.length) { - // ideally this should be no-op and throw (breaking change)... - output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`); - } - - if (missingFeatureInFile.length) { - missingFeatureInFile = [...new Set(missingFeatureInFile)]; - output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`); - } - } - }; - - const presetReporter = opts.reporter || config.reporter; - // use standard reporter - if (!presetReporter) { - mocha.reporter(reporter, opts); - return mocha; - } - - // load custom reporter with options - const reporterOptions = Object.assign(config.reporterOptions || {}); - - if (opts.reporterOptions !== undefined) { - opts.reporterOptions.split(',').forEach((opt) => { - const L = opt.split('='); - if (L.length > 2 || L.length === 0) { - throw new Error(`invalid reporter option '${opt}'`); - } else if (L.length === 2) { - reporterOptions[L[0]] = L[1]; - } else { - reporterOptions[L[0]] = true; - } - }); - } - - const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter'); - if (reporterOptions['codeceptjs-cli-reporter'] && attributes) { - Object.defineProperty( - reporterOptions, - 'codeceptjs/lib/cli', - attributes, - ); - delete reporterOptions['codeceptjs-cli-reporter']; - } - - // custom reporters - mocha.reporter(presetReporter, reporterOptions); - return mocha; - } -} - -module.exports = MochaFactory; diff --git a/lib/output.js b/lib/output.js index 70e9909ab..81df5dcf3 100644 --- a/lib/output.js +++ b/lib/output.js @@ -173,7 +173,9 @@ module.exports = { * @param {Mocha.Test} test */ - started(test) {}, + started(test) { + print(` ${colors.dim.bold('Scenario()')}`); + }, /** * @param {Mocha.Test} test @@ -191,6 +193,18 @@ module.exports = { }, }, + hook: { + started(hook) { + print(` ${colors.dim.bold(hook.toCode())}`); + }, + passed(hook) { + print(); + }, + failed(hook) { + print(` ${colors.red.bold(hook.toCode())}`); + }, + }, + /** * * Print a text in console log diff --git a/lib/scenario.js b/lib/scenario.js deleted file mode 100644 index 1d9c77bc9..000000000 --- a/lib/scenario.js +++ /dev/null @@ -1,203 +0,0 @@ -const promiseRetry = require('promise-retry'); -const event = require('./event'); -const recorder = require('./recorder'); -const assertThrown = require('./assert/throws'); -const { ucfirst, isAsyncFunction } = require('./utils'); -const { getInjectedArguments } = require('./interfaces/inject'); - -const injectHook = function (inject, suite) { - try { - inject(); - } catch (err) { - recorder.throw(err); - } - recorder.catch(err => { - event.emit(event.test.failed, suite, err); - throw err; - }); - return recorder.promise(); -}; - -function makeDoneCallableOnce(done) { - let called = false; - return function (err) { - if (called) { - return; - } - called = true; - return done(err); - }; -} -/** - * Wraps test function, injects support objects from container, - * starts promise chain with recorder, performs before/after hooks - * through event system. - */ -module.exports.test = test => { - const testFn = test.fn; - if (!testFn) { - return test; - } - - test.steps = []; - test.timeout(0); - test.async = true; - - test.fn = function (done) { - const doneFn = makeDoneCallableOnce(done); - recorder.errHandler(err => { - recorder.session.start('teardown'); - recorder.cleanAsyncErr(); - if (test.throws) { - // check that test should actually fail - try { - assertThrown(err, test.throws); - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - recorder.add(doneFn); - return; - } catch (newErr) { - err = newErr; - } - } - event.emit(event.test.failed, test, err); - event.emit(event.test.finished, test); - recorder.add(() => doneFn(err)); - }); - - if (isAsyncFunction(testFn)) { - event.emit(event.test.started, test); - testFn - .call(test, getInjectedArguments(testFn, test)) - .then(() => { - recorder.add('fire test.passed', () => { - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - }); - recorder.add('finish test', doneFn); - }) - .catch(err => { - recorder.throw(err); - }) - .finally(() => { - recorder.catch(); - }); - return; - } - - try { - event.emit(event.test.started, test); - testFn.call(test, getInjectedArguments(testFn, test)); - } catch (err) { - recorder.throw(err); - } finally { - recorder.add('fire test.passed', () => { - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - }); - recorder.add('finish test', doneFn); - recorder.catch(); - } - }; - return test; -}; - -/** - * Injects arguments to function from controller - */ -module.exports.injected = function (fn, suite, hookName) { - return function (done) { - const doneFn = makeDoneCallableOnce(done); - const errHandler = err => { - recorder.session.start('teardown'); - recorder.cleanAsyncErr(); - event.emit(event.test.failed, suite, err); - if (hookName === 'after') event.emit(event.test.after, suite); - if (hookName === 'afterSuite') event.emit(event.suite.after, suite); - recorder.add(() => doneFn(err)); - }; - - recorder.errHandler(err => { - errHandler(err); - }); - - if (!fn) throw new Error('fn is not defined'); - - event.emit(event.hook.started, suite); - - this.test.body = fn.toString(); - - if (!recorder.isRunning()) { - recorder.errHandler(err => { - errHandler(err); - }); - } - - const opts = suite.opts || {}; - const retries = opts[`retry${ucfirst(hookName)}`] || 0; - - promiseRetry( - async (retry, number) => { - try { - recorder.startUnlessRunning(); - await fn.call(this, getInjectedArguments(fn)); - await recorder.promise().catch(err => retry(err)); - } catch (err) { - retry(err); - } finally { - if (number < retries) { - recorder.stop(); - recorder.start(); - } - } - }, - { retries }, - ) - .then(() => { - recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite)); - recorder.add(`finish ${hookName} hook`, doneFn); - recorder.catch(); - }) - .catch(e => { - recorder.throw(e); - recorder.catch(e => { - const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr(); - errHandler(err); - }); - recorder.add('fire hook.failed', () => event.emit(event.hook.failed, suite, e)); - }); - }; -}; - -/** - * Starts promise chain, so helpers could enqueue their hooks - */ -module.exports.setup = function (suite) { - return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.test.before, suite && suite.ctx && suite.ctx.currentTest); - }, suite); -}; - -module.exports.teardown = function (suite) { - return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.test.after, suite && suite.ctx && suite.ctx.currentTest); - }, suite); -}; - -module.exports.suiteSetup = function (suite) { - return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.suite.before, suite); - }, suite); -}; - -module.exports.suiteTeardown = function (suite) { - return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.suite.after, suite); - }, suite); -}; - -module.exports.getInjectedArguments = getInjectedArguments; diff --git a/lib/ui.js b/lib/ui.js deleted file mode 100644 index 2cc8ef305..000000000 --- a/lib/ui.js +++ /dev/null @@ -1,236 +0,0 @@ -const escapeRe = require('escape-string-regexp'); -const Suite = require('mocha/lib/suite'); -const Test = require('mocha/lib/test'); - -const scenario = require('./scenario'); -const ScenarioConfig = require('./interfaces/scenarioConfig'); -const FeatureConfig = require('./interfaces/featureConfig'); -const addDataContext = require('./data/context'); -const container = require('./container'); - -const setContextTranslation = (context) => { - const contexts = container.translation().value('contexts'); - - if (contexts) { - for (const key of Object.keys(contexts)) { - if (context[key]) { - context[contexts[key]] = context[key]; - } - } - } -}; - -/** - * Codecept-style interface: - * - * Feature('login'); - * - * Scenario('login as regular user', ({I}) { - * I.fillField(); - * I.click(); - * I.see('Hello, '+data.login); - * }); - * - * @param {Mocha.Suite} suite Root suite. - * @ignore - */ -module.exports = function (suite) { - const suites = [suite]; - suite.timeout(0); - let afterAllHooks; - let afterEachHooks; - let afterAllHooksAreLoaded; - let afterEachHooksAreLoaded; - - suite.on('pre-require', (context, file, mocha) => { - const common = require('mocha/lib/interfaces/common')(suites, context, mocha); - - const addScenario = function (title, opts = {}, fn) { - const suite = suites[0]; - - if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; - } - if (suite.pending) { - fn = null; - } - const test = new Test(title, fn); - test.fullTitle = () => `${suite.title}: ${test.title}`; - - test.tags = (suite.tags || []).concat(title.match(/(\@[a-zA-Z0-9-_]+)/g) || []); // match tags from title - test.file = file; - if (!test.inject) { - test.inject = {}; - } - - suite.addTest(scenario.test(test)); - if (opts.retries) test.retries(opts.retries); - if (opts.timeout) test.totalTimeout = opts.timeout; - test.opts = opts; - - return new ScenarioConfig(test); - }; - - // create dispatcher - - context.BeforeAll = common.before; - context.AfterAll = common.after; - - context.run = mocha.options.delay && common.runWithSuite(suite); - /** - * Describe a "suite" with the given `title` - * and callback `fn` containing nested suites - * and/or tests. - * @global - * @param {string} title - * @param {Object} [opts] - * @returns {FeatureConfig} - */ - - context.Feature = function (title, opts) { - if (suites.length > 1) { - suites.shift(); - } - - afterAllHooks = []; - afterEachHooks = []; - afterAllHooksAreLoaded = false; - afterEachHooksAreLoaded = false; - - const suite = Suite.create(suites[0], title); - if (!opts) opts = {}; - suite.opts = opts; - suite.timeout(0); - - if (opts.retries) suite.retries(opts.retries); - if (opts.timeout) suite.totalTimeout = opts.timeout; - - suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || []; // match tags from title - suite.file = file; - suite.fullTitle = () => `${suite.title}:`; - suites.unshift(suite); - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); - afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]); - - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); - afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]); - - if (opts.skipInfo && opts.skipInfo.skipped) { - suite.pending = true; - suite.opts = { ...suite.opts, skipInfo: opts.skipInfo }; - } - - return new FeatureConfig(suite); - }; - - /** - * Pending test suite. - * @global - * @kind constant - * @type {CodeceptJS.IFeature} - */ - context.xFeature = context.Feature.skip = function (title, opts) { - const skipInfo = { - skipped: true, - message: 'Skipped due to "skip" on Feature.', - }; - return context.Feature(title, { ...opts, skipInfo }); - }; - - context.BeforeSuite = function (fn) { - suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')); - }; - - context.AfterSuite = function (fn) { - afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]); - }; - - context.Background = context.Before = function (fn) { - suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')); - }; - - context.After = function (fn) { - afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]); - }; - - /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. - * @ignore - */ - context.Scenario = addScenario; - /** - * Exclusive test-case. - * @ignore - */ - context.Scenario.only = function (title, opts, fn) { - const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`; - mocha.grep(new RegExp(reString)); - process.env.SCENARIO_ONLY = true; - return addScenario(title, opts, fn); - }; - - /** - * Pending test case. - * @global - * @kind constant - * @type {CodeceptJS.IScenario} - */ - context.xScenario = context.Scenario.skip = function (title, opts = {}, fn) { - if (typeof opts === 'function' && !fn) { - opts = {}; - } - - return context.Scenario(title, opts); - }; - - /** - * Pending test case with message: 'Test not implemented!'. - * @global - * @kind constant - * @type {CodeceptJS.IScenario} - */ - context.Scenario.todo = function (title, opts = {}, fn) { - if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; - } - - const skipInfo = { - message: 'Test not implemented!', - description: fn ? fn.toString() : '', - }; - - return context.Scenario(title, { ...opts, skipInfo }); - }; - - /** - * For translation - */ - - setContextTranslation(context); - - addDataContext(context); - }); - - suite.on('post-require', () => { - /** - * load hooks from arrays to suite to prevent reordering - */ - if (!afterEachHooksAreLoaded && Array.isArray(afterEachHooks)) { - afterEachHooks.forEach((hook) => { - suites[0].afterEach(hook[0], hook[1]); - }); - afterEachHooksAreLoaded = true; - } - - if (!afterAllHooksAreLoaded && Array.isArray(afterAllHooks)) { - afterAllHooks.forEach((hook) => { - suites[0].afterAll(hook[0], hook[1]); - }); - afterAllHooksAreLoaded = true; - } - }); -}; diff --git a/lib/workers.js b/lib/workers.js index 2f38e627f..c35b94677 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -9,7 +9,7 @@ const { const { EventEmitter } = require('events'); const ms = require('ms'); const Codecept = require('./codecept'); -const MochaFactory = require('./mochaFactory'); +const MochaFactory = require('./mocha/factory'); const Container = require('./container'); const { getTestRoot } = require('./command/utils'); const { isFunction, fileExists } = require('./utils'); diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index ab275e8f9..a2249c633 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -10,8 +10,8 @@ const builder = new Gherkin.AstBuilder(uuidFn); const matcher = new Gherkin.GherkinClassicTokenMatcher(); const Config = require('../../lib/config'); -const { Given, When, And, Then, matchStep, clearSteps, defineParameterType } = require('../../lib/interfaces/bdd'); -const run = require('../../lib/interfaces/gherkin'); +const { Given, When, And, Then, matchStep, clearSteps, defineParameterType } = require('../../lib/mocha/bdd'); +const run = require('../../lib/mocha/gherkin'); const recorder = require('../../lib/recorder'); const container = require('../../lib/container'); const actor = require('../../lib/actor'); diff --git a/test/unit/data/ui_test.js b/test/unit/data/ui_test.js index d05ff0a03..0190e74a2 100644 --- a/test/unit/data/ui_test.js +++ b/test/unit/data/ui_test.js @@ -5,7 +5,7 @@ import('chai').then(chai => { const Mocha = require('mocha/lib/mocha'); const Suite = require('mocha/lib/suite'); -const makeUI = require('../../../lib/ui'); +const makeUI = require('../../../lib/mocha/ui'); const addData = require('../../../lib/data/context'); const DataTable = require('../../../lib/data/table'); const Secret = require('../../../lib/secret'); @@ -31,13 +31,11 @@ describe('ui', () => { describe('Data', () => { it('can add a tag to all scenarios', () => { - const dataScenarioConfig = context.Data(dataTable) - .Scenario('scenario', () => { - }); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); dataScenarioConfig.tag('@user'); - dataScenarioConfig.scenarios.forEach((scenario) => { + dataScenarioConfig.scenarios.forEach(scenario => { expect(scenario.test.tags).to.include('@user'); }); }); @@ -89,7 +87,7 @@ describe('ui', () => { it("should shows object's toString() method in each scenario's name if the toString() method is overridden", () => { const data = [{ toString: () => 'test case title' }]; - const dataScenarioConfig = context.Data(data).Scenario('scenario', () => { }); + const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {}); expect('scenario | test case title').to.equal(dataScenarioConfig.scenarios[0].test.title); }); diff --git a/test/unit/scenario_test.js b/test/unit/scenario_test.js index c9f5c4af7..c97cf91a9 100644 --- a/test/unit/scenario_test.js +++ b/test/unit/scenario_test.js @@ -1,99 +1,99 @@ -let expect -import('chai').then((chai) => { - expect = chai.expect -}) -const sinon = require('sinon') +let expect; +import('chai').then(chai => { + expect = chai.expect; +}); +const sinon = require('sinon'); -const scenario = require('../../lib/scenario') -const recorder = require('../../lib/recorder') -const event = require('../../lib/event') +const scenario = require('../../lib/mocha/scenario'); +const recorder = require('../../lib/recorder'); +const event = require('../../lib/event'); -let test -let fn -let before -let after -let beforeSuite -let afterSuite -let failed -let started +let test; +let fn; +let before; +let after; +let beforeSuite; +let afterSuite; +let failed; +let started; describe('Scenario', () => { beforeEach(() => { - test = { timeout: () => {} } - fn = sinon.spy() - test.fn = fn - }) - beforeEach(() => recorder.reset()) - afterEach(() => event.cleanDispatcher()) + test = { timeout: () => {} }; + fn = sinon.spy(); + test.fn = fn; + }); + beforeEach(() => recorder.reset()); + afterEach(() => event.cleanDispatcher()); it('should wrap test function', () => { - scenario.test(test).fn(() => {}) - expect(fn.called).is.ok - }) + scenario.test(test).fn(() => {}); + expect(fn.called).is.ok; + }); it('should work with async func', () => { - let counter = 0 + let counter = 0; test.fn = () => { recorder.add('test', async () => { - await counter++ - await counter++ - await counter++ - counter++ - }) - } + await counter++; + await counter++; + await counter++; + counter++; + }); + }; - scenario.setup() - scenario.test(test).fn(() => null) - recorder.add('validation', () => expect(counter).to.eq(4)) - return recorder.promise() - }) + scenario.setup(); + scenario.test(test).fn(() => null); + recorder.add('validation', () => expect(counter).to.eq(4)); + return recorder.promise(); + }); describe('events', () => { beforeEach(() => { - event.dispatcher.on(event.test.before, (before = sinon.spy())) - event.dispatcher.on(event.test.after, (after = sinon.spy())) - event.dispatcher.on(event.test.started, (started = sinon.spy())) - event.dispatcher.on(event.suite.before, (beforeSuite = sinon.spy())) - event.dispatcher.on(event.suite.after, (afterSuite = sinon.spy())) - scenario.suiteSetup() - scenario.setup() - }) + event.dispatcher.on(event.test.before, (before = sinon.spy())); + event.dispatcher.on(event.test.after, (after = sinon.spy())); + event.dispatcher.on(event.test.started, (started = sinon.spy())); + event.dispatcher.on(event.suite.before, (beforeSuite = sinon.spy())); + event.dispatcher.on(event.suite.after, (afterSuite = sinon.spy())); + scenario.suiteSetup(); + scenario.setup(); + }); it('should fire events', () => { - scenario.test(test).fn(() => null) - expect(started.called).is.ok - scenario.teardown() - scenario.suiteTeardown() + scenario.test(test).fn(() => null); + expect(started.called).is.ok; + scenario.teardown(); + scenario.suiteTeardown(); return recorder .promise() .then(() => expect(beforeSuite.called).is.ok) .then(() => expect(afterSuite.called).is.ok) .then(() => expect(before.called).is.ok) - .then(() => expect(after.called).is.ok) - }) + .then(() => expect(after.called).is.ok); + }); it('should fire failed event on error', () => { - event.dispatcher.on(event.test.failed, (failed = sinon.spy())) - scenario.setup() + event.dispatcher.on(event.test.failed, (failed = sinon.spy())); + scenario.setup(); test.fn = () => { - throw new Error('ups') - } - scenario.test(test).fn(() => {}) + throw new Error('ups'); + }; + scenario.test(test).fn(() => {}); return recorder .promise() .then(() => expect(failed.called).is.ok) - .catch(() => null) - }) + .catch(() => null); + }); it('should fire failed event on async error', () => { test.fn = () => { - recorder.throw(new Error('ups')) - } - scenario.test(test).fn(() => {}) + recorder.throw(new Error('ups')); + }; + scenario.test(test).fn(() => {}); return recorder .promise() .then(() => expect(failed.called).is.ok) - .catch(() => null) - }) - }) -}) + .catch(() => null); + }); + }); +}); diff --git a/test/unit/ui_test.js b/test/unit/ui_test.js index 5d3e0989e..eb1bf5889 100644 --- a/test/unit/ui_test.js +++ b/test/unit/ui_test.js @@ -1,240 +1,220 @@ -let expect -import('chai').then((chai) => { - expect = chai.expect -}) -const Mocha = require('mocha/lib/mocha') -const Suite = require('mocha/lib/suite') +let expect; +import('chai').then(chai => { + expect = chai.expect; +}); +const Mocha = require('mocha/lib/mocha'); +const Suite = require('mocha/lib/suite'); -global.codeceptjs = require('../../lib') -const makeUI = require('../../lib/ui') +global.codeceptjs = require('../../lib'); +const makeUI = require('../../lib/mocha/ui'); describe('ui', () => { - let suite - let context + let suite; + let context; beforeEach(() => { - context = {} - suite = new Suite('empty') - makeUI(suite) - suite.emit('pre-require', context, {}, new Mocha()) - }) + context = {}; + suite = new Suite('empty'); + makeUI(suite); + suite.emit('pre-require', context, {}, new Mocha()); + }); describe('basic constants', () => { - const constants = ['Before', 'Background', 'BeforeAll', 'After', 'AfterAll', 'Scenario', 'xScenario'] + const constants = ['Before', 'Background', 'BeforeAll', 'After', 'AfterAll', 'Scenario', 'xScenario']; - constants.forEach((c) => { - it(`context should contain ${c}`, () => expect(context[c]).is.ok) - }) - }) + constants.forEach(c => { + it(`context should contain ${c}`, () => expect(context[c]).is.ok); + }); + }); describe('Feature', () => { - let suiteConfig + let suiteConfig; it('Feature should return featureConfig', () => { - suiteConfig = context.Feature('basic suite') - expect(suiteConfig.suite).is.ok - }) + suiteConfig = context.Feature('basic suite'); + expect(suiteConfig.suite).is.ok; + }); it('should contain title', () => { - suiteConfig = context.Feature('basic suite') - expect(suiteConfig.suite).is.ok - expect(suiteConfig.suite.title).eq('basic suite') - expect(suiteConfig.suite.fullTitle()).eq('basic suite:') - }) + suiteConfig = context.Feature('basic suite'); + expect(suiteConfig.suite).is.ok; + expect(suiteConfig.suite.title).eq('basic suite'); + expect(suiteConfig.suite.fullTitle()).eq('basic suite:'); + }); it('should contain tags', () => { - suiteConfig = context.Feature('basic suite') - expect(0).eq(suiteConfig.suite.tags.length) + suiteConfig = context.Feature('basic suite'); + expect(0).eq(suiteConfig.suite.tags.length); - suiteConfig = context.Feature('basic suite @very @important') - expect(suiteConfig.suite).is.ok + suiteConfig = context.Feature('basic suite @very @important'); + expect(suiteConfig.suite).is.ok; - suiteConfig.suite.tags.should.include('@very') - suiteConfig.suite.tags.should.include('@important') + suiteConfig.suite.tags.should.include('@very'); + suiteConfig.suite.tags.should.include('@important'); - suiteConfig.tag('@user') - suiteConfig.suite.tags.should.include('@user') + suiteConfig.tag('@user'); + suiteConfig.suite.tags.should.include('@user'); - suiteConfig.suite.tags.should.not.include('@slow') - suiteConfig.tag('slow') - suiteConfig.suite.tags.should.include('@slow') - }) + suiteConfig.suite.tags.should.not.include('@slow'); + suiteConfig.tag('slow'); + suiteConfig.suite.tags.should.include('@slow'); + }); it('retries can be set', () => { - suiteConfig = context.Feature('basic suite') - suiteConfig.retry(3) - expect(3).eq(suiteConfig.suite.retries()) - }) + suiteConfig = context.Feature('basic suite'); + suiteConfig.retry(3); + expect(3).eq(suiteConfig.suite.retries()); + }); it('timeout can be set', () => { - suiteConfig = context.Feature('basic suite') - expect(0).eq(suiteConfig.suite.timeout()) - suiteConfig.timeout(3) - expect(3).eq(suiteConfig.suite.timeout()) - }) + suiteConfig = context.Feature('basic suite'); + expect(0).eq(suiteConfig.suite.timeout()); + suiteConfig.timeout(3); + expect(3).eq(suiteConfig.suite.timeout()); + }); it('helpers can be configured', () => { - suiteConfig = context.Feature('basic suite') - expect(!suiteConfig.suite.config) - suiteConfig.config('WebDriver', { browser: 'chrome' }) - expect('chrome').eq(suiteConfig.suite.config.WebDriver.browser) - suiteConfig.config({ browser: 'firefox' }) - expect('firefox').eq(suiteConfig.suite.config[0].browser) + suiteConfig = context.Feature('basic suite'); + expect(!suiteConfig.suite.config); + suiteConfig.config('WebDriver', { browser: 'chrome' }); + expect('chrome').eq(suiteConfig.suite.config.WebDriver.browser); + suiteConfig.config({ browser: 'firefox' }); + expect('firefox').eq(suiteConfig.suite.config[0].browser); suiteConfig.config('WebDriver', () => { - return { browser: 'edge' } - }) - expect('edge').eq(suiteConfig.suite.config.WebDriver.browser) - }) + return { browser: 'edge' }; + }); + expect('edge').eq(suiteConfig.suite.config.WebDriver.browser); + }); it('Feature can be skipped', () => { - suiteConfig = context.Feature.skip('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) + suiteConfig = context.Feature.skip('skipped suite'); + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); + }); it('Feature can be skipped via xFeature', () => { - suiteConfig = context.xFeature('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) + suiteConfig = context.xFeature('skipped suite'); + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); + }); it('Feature are not skipped by default', () => { - suiteConfig = context.Feature('not skipped suite') - expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true') + suiteConfig = context.Feature('not skipped suite'); + expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true'); // expect(suiteConfig.suite.opts, undefined, 'Features should have no skip info'); - }) + }); it('Feature can be skipped', () => { - suiteConfig = context.Feature.skip('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) + suiteConfig = context.Feature.skip('skipped suite'); + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); + }); it('Feature can be skipped via xFeature', () => { - suiteConfig = context.xFeature('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) + suiteConfig = context.xFeature('skipped suite'); + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); + }); it('Feature are not skipped by default', () => { - suiteConfig = context.Feature('not skipped suite') - expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true') - // expect(suiteConfig.suite.opts, undefined, 'Features should have no skip info'); - }) - - it('Feature can be skipped', () => { - suiteConfig = context.Feature.skip('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) - - it('Feature can be skipped via xFeature', () => { - suiteConfig = context.xFeature('skipped suite') - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') - }) - - it('Feature are not skipped by default', () => { - suiteConfig = context.Feature('not skipped suite') - expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true') - expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info') - }) + suiteConfig = context.Feature('not skipped suite'); + expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true'); + expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info'); + }); it('Feature should correctly pass options to suite context', () => { - suiteConfig = context.Feature('not skipped suite', { key: 'value' }) - expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options') - }) - }) + suiteConfig = context.Feature('not skipped suite', { key: 'value' }); + expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options'); + }); + }); describe('Scenario', () => { - let scenarioConfig + let scenarioConfig; it('Scenario should return scenarioConfig', () => { - scenarioConfig = context.Scenario('basic scenario') - expect(scenarioConfig.test).is.ok - }) + scenarioConfig = context.Scenario('basic scenario'); + expect(scenarioConfig.test).is.ok; + }); it('should contain title', () => { - context.Feature('suite') - scenarioConfig = context.Scenario('scenario') - expect(scenarioConfig.test.title).eq('scenario') - expect(scenarioConfig.test.fullTitle()).eq('suite: scenario') - expect(scenarioConfig.test.tags.length).eq(0) - }) + context.Feature('suite'); + scenarioConfig = context.Scenario('scenario'); + expect(scenarioConfig.test.title).eq('scenario'); + expect(scenarioConfig.test.fullTitle()).eq('suite: scenario'); + expect(scenarioConfig.test.tags.length).eq(0); + }); it('should contain tags', () => { - context.Feature('basic suite @cool') + context.Feature('basic suite @cool'); - scenarioConfig = context.Scenario('scenario @very @important') + scenarioConfig = context.Scenario('scenario @very @important'); - scenarioConfig.test.tags.should.include('@cool') - scenarioConfig.test.tags.should.include('@very') - scenarioConfig.test.tags.should.include('@important') + scenarioConfig.test.tags.should.include('@cool'); + scenarioConfig.test.tags.should.include('@very'); + scenarioConfig.test.tags.should.include('@important'); - scenarioConfig.tag('@user') - scenarioConfig.test.tags.should.include('@user') - }) + scenarioConfig.tag('@user'); + scenarioConfig.test.tags.should.include('@user'); + }); it('should dynamically inject dependencies', () => { - scenarioConfig = context.Scenario('scenario') - scenarioConfig.injectDependencies({ Data: 'data' }) - expect(scenarioConfig.test.inject.Data).eq('data') - }) + scenarioConfig = context.Scenario('scenario'); + scenarioConfig.injectDependencies({ Data: 'data' }); + expect(scenarioConfig.test.inject.Data).eq('data'); + }); describe('todo', () => { it('should inject skipInfo to opts', () => { scenarioConfig = context.Scenario.todo('scenario', () => { - console.log('Scenario Body') - }) + console.log('Scenario Body'); + }); - expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true') - expect(scenarioConfig.test.opts.skipInfo.message).eq('Test not implemented!') - expect(scenarioConfig.test.opts.skipInfo.description).to.include("console.log('Scenario Body')") - }) + expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true'); + expect(scenarioConfig.test.opts.skipInfo.message).eq('Test not implemented!'); + expect(scenarioConfig.test.opts.skipInfo.description).to.include("console.log('Scenario Body')"); + }); it('should contain empty description in skipInfo and empty body', () => { - scenarioConfig = context.Scenario.todo('scenario') + scenarioConfig = context.Scenario.todo('scenario'); - expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true') - expect(scenarioConfig.test.opts.skipInfo.description).eq('') - expect(scenarioConfig.test.body).eq('') - }) + expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true'); + expect(scenarioConfig.test.opts.skipInfo.description).eq(''); + expect(scenarioConfig.test.body).eq(''); + }); it('should inject custom opts to opts and without callback', () => { - scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }) + scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }); - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') - }) + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); + }); it('should inject custom opts to opts and with callback', () => { scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }, () => { - console.log('Scenario Body') - }) + console.log('Scenario Body'); + }); - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') - }) - }) + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); + }); + }); describe('skip', () => { it('should inject custom opts to opts and without callback', () => { - scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }) + scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }); - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') - }) + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); + }); it('should inject custom opts to opts and with callback', () => { scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }, () => { - console.log('Scenario Body') - }) - - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') - }) - }) - }) -}) + console.log('Scenario Body'); + }); + + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); + }); + }); + }); +}); From 41453d05dfee9cc2194c7c6de77d84e8219d0c2e Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 05:19:39 +0200 Subject: [PATCH 02/13] refactored mocha classes, added class hooks --- lib/listener/steps.js | 1 + lib/mocha/bdd.js | 81 ++++++++++++ lib/mocha/cli.js | 257 ++++++++++++++++++++++++++++++++++++ lib/mocha/factory.js | 107 +++++++++++++++ lib/mocha/featureConfig.js | 63 +++++++++ lib/mocha/gherkin.js | 196 +++++++++++++++++++++++++++ lib/mocha/hooks.js | 59 +++++++++ lib/mocha/inject.js | 24 ++++ lib/mocha/scenario.js | 204 ++++++++++++++++++++++++++++ lib/mocha/scenarioConfig.js | 92 +++++++++++++ lib/mocha/ui.js | 236 +++++++++++++++++++++++++++++++++ lib/output.js | 4 + 12 files changed, 1324 insertions(+) create mode 100644 lib/mocha/bdd.js create mode 100644 lib/mocha/cli.js create mode 100644 lib/mocha/factory.js create mode 100644 lib/mocha/featureConfig.js create mode 100644 lib/mocha/gherkin.js create mode 100644 lib/mocha/hooks.js create mode 100644 lib/mocha/inject.js create mode 100644 lib/mocha/scenario.js create mode 100644 lib/mocha/scenarioConfig.js create mode 100644 lib/mocha/ui.js diff --git a/lib/listener/steps.js b/lib/listener/steps.js index ea999cbec..106744288 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -21,6 +21,7 @@ module.exports = function () { currentTest.steps = []; if (!('retryNum' in currentTest)) currentTest.retryNum = 0; else currentTest.retryNum += 1; + output.scenario.started(test); }); event.dispatcher.on(event.test.after, test => { diff --git a/lib/mocha/bdd.js b/lib/mocha/bdd.js new file mode 100644 index 000000000..c320d3af6 --- /dev/null +++ b/lib/mocha/bdd.js @@ -0,0 +1,81 @@ +const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('@cucumber/cucumber-expressions'); +const Config = require('../config'); + +let steps = {}; + +const STACK_POSITION = 2; + +/** + * @param {*} step + * @param {*} fn + */ +const addStep = (step, fn) => { + const avoidDuplicateSteps = Config.get('gherkin', {}).avoidDuplicateSteps || false; + const stack = new Error().stack; + if (avoidDuplicateSteps && steps[step]) { + throw new Error(`Step '${step}' is already defined`); + } + steps[step] = fn; + fn.line = stack && stack.split('\n')[STACK_POSITION]; + if (fn.line) { + fn.line = fn.line + .trim() + .replace(/^at (.*?)\(/, '(') + .replace(codecept_dir, '.'); + } +}; + +const parameterTypeRegistry = new ParameterTypeRegistry(); + +const matchStep = step => { + for (const stepName in steps) { + if (stepName.indexOf('/') === 0) { + const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || []; + const res = step.match(new RegExp(regExpArr[1], regExpArr[2])); + if (res) { + const fn = steps[stepName]; + fn.params = res.slice(1); + return fn; + } + continue; + } + const expression = new CucumberExpression(stepName, parameterTypeRegistry); + const res = expression.match(step); + if (res) { + const fn = steps[stepName]; + fn.params = res.map(arg => arg.getValue()); + return fn; + } + } + throw new Error(`No steps matching "${step.toString()}"`); +}; + +const clearSteps = () => { + steps = {}; +}; + +const getSteps = () => { + return steps; +}; + +const defineParameterType = options => { + const parameterType = buildParameterType(options); + parameterTypeRegistry.defineParameterType(parameterType); +}; + +const buildParameterType = ({ name, regexp, transformer, useForSnippets, preferForRegexpMatch }) => { + if (typeof useForSnippets !== 'boolean') useForSnippets = true; + if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false; + return new ParameterType(name, regexp, null, transformer, useForSnippets, preferForRegexpMatch); +}; + +module.exports = { + Given: addStep, + When: addStep, + Then: addStep, + And: addStep, + matchStep, + getSteps, + clearSteps, + defineParameterType, +}; diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js new file mode 100644 index 000000000..ec132d6e3 --- /dev/null +++ b/lib/mocha/cli.js @@ -0,0 +1,257 @@ +const { + reporters: { Base }, +} = require('mocha'); +const ms = require('ms'); +const event = require('../event'); +const AssertionFailedError = require('../assert/error'); +const output = require('../output'); + +const cursor = Base.cursor; +let currentMetaStep = []; +let codeceptjsEventDispatchersRegistered = false; + +class Cli extends Base { + constructor(runner, opts) { + super(runner); + let level = 0; + this.loadedTests = []; + opts = opts.reporterOptions || opts; + if (opts.steps) level = 1; + if (opts.debug) level = 2; + if (opts.verbose) level = 3; + output.level(level); + output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`); + output.print(`Using test root "${global.codecept_dir}"`); + + const showSteps = level >= 1; + + if (level >= 2) { + const Containter = require('../container'); + output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)); + output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)); + } + + runner.on('start', () => { + console.log(); + }); + + runner.on('suite', suite => { + output.suite.started(suite); + }); + + runner.on('fail', test => { + if (test.ctx.currentTest) { + this.loadedTests.push(test.ctx.currentTest.uid); + } + if (showSteps && test.steps) { + return output.scenario.failed(test); + } + cursor.CR(); + output.test.failed(test); + }); + + runner.on('pending', test => { + if (test.parent && test.parent.pending) { + const suite = test.parent; + const skipInfo = suite.opts.skipInfo || {}; + skipTestConfig(test, skipInfo.message); + } else { + skipTestConfig(test, null); + } + this.loadedTests.push(test.uid); + cursor.CR(); + output.test.skipped(test); + }); + + runner.on('pass', test => { + if (showSteps && test.steps) { + return output.scenario.passed(test); + } + cursor.CR(); + output.test.passed(test); + }); + + if (showSteps) { + runner.on('test', test => { + currentMetaStep = []; + if (test.steps) { + output.test.started(test); + } + }); + + if (!codeceptjsEventDispatchersRegistered) { + codeceptjsEventDispatchersRegistered = true; + + event.dispatcher.on(event.bddStep.started, step => { + output.stepShift = 2; + output.step(step); + }); + + event.dispatcher.on(event.step.started, step => { + let processingStep = step; + const metaSteps = []; + while (processingStep.metaStep) { + metaSteps.unshift(processingStep.metaStep); + processingStep = processingStep.metaStep; + } + const shift = metaSteps.length; + + for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) { + if (currentMetaStep[i] !== metaSteps[i]) { + output.stepShift = 3 + 2 * i; + if (!metaSteps[i]) continue; + // bdd steps are handled by bddStep.started + if (metaSteps[i].isBDD()) continue; + output.step(metaSteps[i]); + } + } + currentMetaStep = metaSteps; + output.stepShift = 3 + 2 * shift; + if (step.helper.constructor.name !== 'ExpectHelper') { + output.step(step); + } + }); + + event.dispatcher.on(event.step.finished, () => { + output.stepShift = 0; + }); + } + } + + runner.on('suite end', suite => { + let skippedCount = 0; + const grep = runner._grep; + for (const test of suite.tests) { + if (!test.state && !this.loadedTests.includes(test.uid)) { + if (matchTest(grep, test.title)) { + if (!test.opts) { + test.opts = {}; + } + if (!test.opts.skipInfo) { + test.opts.skipInfo = {}; + } + skipTestConfig(test, "Skipped due to failure in 'before' hook"); + output.test.skipped(test); + skippedCount += 1; + } + } + } + + this.stats.pending += skippedCount; + this.stats.tests += skippedCount; + }); + + runner.on('end', this.result.bind(this)); + } + + result() { + const stats = this.stats; + stats.failedHooks = 0; + console.log(); + + // passes + if (stats.failures) { + output.print(output.styles.bold('-- FAILURES:')); + } + + const failuresLog = []; + + // failures + if (stats.failures) { + // append step traces + this.failures.map(test => { + const err = test.err; + + let log = ''; + + if (err instanceof AssertionFailedError) { + err.message = err.inspect(); + } + + const steps = test.steps || (test.ctx && test.ctx.test.steps); + + if (steps && steps.length) { + let scenarioTrace = ''; + steps.reverse().forEach(step => { + const line = `- ${step.toCode()} ${step.line()}`; + // if (step.status === 'failed') line = '' + line; + scenarioTrace += `\n${line}`; + }); + log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; + } + + // display artifacts in debug mode + if (test?.artifacts && Object.keys(test.artifacts).length) { + log += `\n${output.styles.bold('Artifacts:')}`; + for (const artifact of Object.keys(test.artifacts)) { + log += `\n- ${artifact}: ${test.artifacts[artifact]}`; + } + } + + try { + let stack = err.stack ? err.stack.split('\n') : []; + if (stack[0] && stack[0].includes(err.message)) { + stack.shift(); + } + + if (output.level() < 3) { + stack = stack.slice(0, 3); + } + + err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; + + // clone err object so stack trace adjustments won't affect test other reports + test.err = err; + return test; + } catch (e) { + throw Error(e); + } + }); + + const originalLog = Base.consoleLog; + Base.consoleLog = (...data) => { + failuresLog.push([...data]); + originalLog(...data); + }; + Base.list(this.failures); + Base.consoleLog = originalLog; + console.log(); + } + + this.failures.forEach(failure => { + if (failure.constructor.name === 'Hook') { + stats.failedHooks += 1; + } + }); + event.emit(event.all.failures, { failuresLog, stats }); + output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks); + + if (stats.failures && output.level() < 3) { + output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); + } + } +} + +function matchTest(grep, test) { + if (grep) { + return grep.test(test); + } + return true; +} + +function skipTestConfig(test, message) { + if (!test.opts) { + test.opts = {}; + } + if (!test.opts.skipInfo) { + test.opts.skipInfo = {}; + } + test.opts.skipInfo.message = test.opts.skipInfo.message || message; + test.opts.skipInfo.isFastSkipped = true; + event.emit(event.test.skipped, test); + test.state = 'skipped'; +} + +module.exports = function (runner, opts) { + return new Cli(runner, opts); +}; diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js new file mode 100644 index 000000000..07fc70368 --- /dev/null +++ b/lib/mocha/factory.js @@ -0,0 +1,107 @@ +const Mocha = require('mocha'); +const fsPath = require('path'); +const fs = require('fs'); +const reporter = require('./cli'); +const gherkinParser = require('./gherkin'); +const output = require('../output'); +const { genTestId } = require('../utils'); +const ConnectionRefused = require('../helper/errors/ConnectionRefused'); + +const scenarioUi = fsPath.join(__dirname, './ui.js'); + +let mocha; + +class MochaFactory { + static create(config, opts) { + mocha = new Mocha(Object.assign(config, opts)); + output.process(opts.child); + mocha.ui(scenarioUi); + + Mocha.Runner.prototype.uncaught = function (err) { + if (err) { + if (err.toString().indexOf('ECONNREFUSED') >= 0) { + err = new ConnectionRefused(err); + } + output.error(err); + output.print(err.stack); + process.exit(1); + } + output.error('Uncaught undefined exception'); + process.exit(1); + }; + + mocha.loadFiles = fn => { + // load features + if (mocha.suite.suites.length === 0) { + mocha.files.filter(file => file.match(/\.feature$/)).forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file))); + + // remove feature files + mocha.files = mocha.files.filter(file => !file.match(/\.feature$/)); + + Mocha.prototype.loadFiles.call(mocha, fn); + + // add ids for each test and check uniqueness + const dupes = []; + let missingFeatureInFile = []; + const seenTests = []; + mocha.suite.eachTest(test => { + test.uid = genTestId(test); + + const name = test.fullTitle(); + if (seenTests.includes(test.uid)) { + dupes.push(name); + } + seenTests.push(test.uid); + + if (name.slice(0, name.indexOf(':')) === '') { + missingFeatureInFile.push(test.file); + } + }); + if (dupes.length) { + // ideally this should be no-op and throw (breaking change)... + output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`); + } + + if (missingFeatureInFile.length) { + missingFeatureInFile = [...new Set(missingFeatureInFile)]; + output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`); + } + } + }; + + const presetReporter = opts.reporter || config.reporter; + // use standard reporter + if (!presetReporter) { + mocha.reporter(reporter, opts); + return mocha; + } + + // load custom reporter with options + const reporterOptions = Object.assign(config.reporterOptions || {}); + + if (opts.reporterOptions !== undefined) { + opts.reporterOptions.split(',').forEach(opt => { + const L = opt.split('='); + if (L.length > 2 || L.length === 0) { + throw new Error(`invalid reporter option '${opt}'`); + } else if (L.length === 2) { + reporterOptions[L[0]] = L[1]; + } else { + reporterOptions[L[0]] = true; + } + }); + } + + const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter'); + if (reporterOptions['codeceptjs-cli-reporter'] && attributes) { + Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes); + delete reporterOptions['codeceptjs-cli-reporter']; + } + + // custom reporters + mocha.reporter(presetReporter, reporterOptions); + return mocha; + } +} + +module.exports = MochaFactory; diff --git a/lib/mocha/featureConfig.js b/lib/mocha/featureConfig.js new file mode 100644 index 000000000..c87ebe187 --- /dev/null +++ b/lib/mocha/featureConfig.js @@ -0,0 +1,63 @@ +/** + * Configuration for a Feature. + * Can inject values and add custom configuration. + */ +class FeatureConfig { + constructor(suite) { + this.suite = suite; + } + + /** + * Retry this test for number of times + * + * @param {number} retries + * @returns {this} + */ + retry(retries) { + this.suite.retries(retries); + return this; + } + + /** + * Set timeout for this test + * @param {number} timeout + * @returns {this} + */ + timeout(timeout) { + this.suite.timeout(timeout); + return this; + } + + /** + * Configures a helper. + * Helper name can be omitted and values will be applied to first helper. + */ + config(helper, obj) { + if (!obj) { + obj = helper; + helper = 0; + } + if (typeof obj === 'function') { + obj = obj(this.suite); + } + if (!this.suite.config) { + this.suite.config = {}; + } + this.suite.config[helper] = obj; + return this; + } + + /** + * Append a tag name to scenario title + * @param {string} tagName + * @returns {this} + */ + tag(tagName) { + if (tagName[0] !== '@') tagName = `@${tagName}`; + if (!this.suite.tags) this.suite.tags = []; + this.suite.tags.push(tagName); + return this; + } +} + +module.exports = FeatureConfig; diff --git a/lib/mocha/gherkin.js b/lib/mocha/gherkin.js new file mode 100644 index 000000000..200db3834 --- /dev/null +++ b/lib/mocha/gherkin.js @@ -0,0 +1,196 @@ +const Gherkin = require('@cucumber/gherkin'); +const Messages = require('@cucumber/messages'); +const { Context, Suite, Test } = require('mocha'); +const debug = require('debug')('codeceptjs:bdd'); + +const { matchStep } = require('./bdd'); +const event = require('../event'); +const scenario = require('./scenario'); +const Step = require('../step'); +const DataTableArgument = require('../data/dataTableArgument'); +const transform = require('../transform'); + +const uuidFn = Messages.IdGenerator.uuid(); +const builder = new Gherkin.AstBuilder(uuidFn); +const matcher = new Gherkin.GherkinClassicTokenMatcher(); +const parser = new Gherkin.Parser(builder, matcher); +parser.stopAtFirstError = false; + +module.exports = (text, file) => { + const ast = parser.parse(text); + let currentLanguage; + + if (ast.feature) { + currentLanguage = getTranslation(ast.feature.language); + } + + if (!ast.feature) { + throw new Error(`No 'Features' available in Gherkin '${file}' provided!`); + } + const suite = new Suite(ast.feature.name, new Context()); + const tags = ast.feature.tags.map(t => t.name); + suite.title = `${suite.title} ${tags.join(' ')}`.trim(); + suite.tags = tags || []; + suite.comment = ast.feature.description; + suite.feature = ast.feature; + suite.file = file; + suite.timeout(0); + + suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); + suite.afterEach('codeceptjs.after', () => scenario.teardown(suite)); + suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); + suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)); + + const runSteps = async steps => { + for (const step of steps) { + const metaStep = new Step.MetaStep(null, step.text); + metaStep.actor = step.keyword.trim(); + let helperStep; + const setMetaStep = step => { + helperStep = step; + if (step.metaStep) { + if (step.metaStep === metaStep) { + return; + } + setMetaStep(step.metaStep); + return; + } + step.metaStep = metaStep; + }; + const fn = matchStep(step.text); + + if (step.dataTable) { + fn.params.push({ + ...step.dataTable, + parse: () => new DataTableArgument(step.dataTable), + }); + metaStep.comment = `\n${transformTable(step.dataTable)}`; + } + + if (step.docString) { + fn.params.push(step.docString); + metaStep.comment = `\n"""\n${step.docString.content}\n"""`; + } + + step.startTime = Date.now(); + step.match = fn.line; + event.emit(event.bddStep.before, step); + event.emit(event.bddStep.started, metaStep); + event.dispatcher.prependListener(event.step.before, setMetaStep); + try { + debug(`Step '${step.text}' started...`); + await fn(...fn.params); + debug('Step passed'); + step.status = 'passed'; + } catch (err) { + debug(`Step failed: ${err?.message}`); + step.status = 'failed'; + step.err = err; + throw err; + } finally { + step.endTime = Date.now(); + event.dispatcher.removeListener(event.step.before, setMetaStep); + } + event.emit(event.bddStep.finished, metaStep); + event.emit(event.bddStep.after, step); + } + }; + + for (const child of ast.feature.children) { + if (child.background) { + suite.beforeEach( + 'Before', + scenario.injected(async () => runSteps(child.background.steps), suite, 'before'), + ); + continue; + } + if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) { + for (const examples of child.scenario.examples) { + const fields = examples.tableHeader.cells.map(c => c.value); + for (const example of examples.tableBody) { + let exampleSteps = [...child.scenario.steps]; + const current = {}; + for (const index in example.cells) { + const placeholder = fields[index]; + const value = transform('gherkin.examples', example.cells[index].value); + example.cells[index].value = value; + current[placeholder] = value; + exampleSteps = exampleSteps.map(step => { + step = { ...step }; + step.text = step.text.split(`<${placeholder}>`).join(value); + return step; + }); + } + const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name)); + let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim(); + + for (const [key, value] of Object.entries(current)) { + if (title.includes(`<${key}>`)) { + title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value); + } + } + + const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))); + test.tags = suite.tags.concat(tags); + test.file = file; + suite.addTest(scenario.test(test)); + } + } + continue; + } + + if (child.scenario) { + const tags = child.scenario.tags.map(t => t.name); + const title = `${child.scenario.name} ${tags.join(' ')}`.trim(); + const test = new Test(title, async () => runSteps(child.scenario.steps)); + test.tags = suite.tags.concat(tags); + test.file = file; + suite.addTest(scenario.test(test)); + } + } + + return suite; +}; + +function transformTable(table) { + let str = ''; + for (const id in table.rows) { + const cells = table.rows[id].cells; + str += cells + .map(c => c.value) + .map(c => c.padEnd(15)) + .join(' | '); + str += '\n'; + } + return str; +} +function addExampleInTable(exampleSteps, placeholders) { + const steps = JSON.parse(JSON.stringify(exampleSteps)); + for (const placeholder in placeholders) { + steps.map(step => { + step = { ...step }; + if (step.dataTable) { + for (const id in step.dataTable.rows) { + const cells = step.dataTable.rows[id].cells; + cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder]))); + } + } + return step; + }); + } + return steps; +} + +function getTranslation(language) { + const translations = Object.keys(require('../../translations')); + + for (const availableTranslation of translations) { + if (!language) { + break; + } + + if (availableTranslation.includes(language)) { + return require('../../translations')[availableTranslation]; + } + } +} diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js new file mode 100644 index 000000000..48dad37d9 --- /dev/null +++ b/lib/mocha/hooks.js @@ -0,0 +1,59 @@ +const event = require('../event'); + +class Hook { + constructor(context, error) { + this.suite = context.suite; + this.test = context.test; + this.runnable = context.ctx.test; + this.ctx = context.ctx; + this.error = error; + } + + toString() { + return this.constructor.name.replace('Hook', ''); + } + + toCode() { + return this.toString() + '()'; + } + + get name() { + return this.constructor.name; + } +} + +class BeforeHook extends Hook {} + +class AfterHook extends Hook {} + +class BeforeSuiteHook extends Hook {} + +class AfterSuiteHook extends Hook {} + +function fireHook(eventType, suite, error) { + const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1]; + switch (hook) { + case 'before each': + event.emit(eventType, new BeforeHook(suite)); + break; + case 'after each': + event.emit(eventType, new AfterHook(suite, error)); + break; + case 'before all': + event.emit(eventType, new BeforeSuiteHook(suite)); + break; + case 'after all': + event.emit(eventType, new AfterSuiteHook(suite, error)); + break; + default: + event.emit(eventType, suite, error); + } +} + +module.exports = { + BeforeHook, + AfterHook, + BeforeSuiteHook, + AfterSuiteHook, + fireHook, +}; diff --git a/lib/mocha/inject.js b/lib/mocha/inject.js new file mode 100644 index 000000000..712020ba3 --- /dev/null +++ b/lib/mocha/inject.js @@ -0,0 +1,24 @@ +const parser = require('../parser'); + +const getInjectedArguments = (fn, test) => { + const container = require('../container'); + const testArgs = {}; + const params = parser.getParams(fn) || []; + const objects = container.support(); + for (const key of params) { + testArgs[key] = {}; + if (test && test.inject && test.inject[key]) { + // @FIX: need fix got inject + testArgs[key] = test.inject[key]; + continue; + } + if (!objects[key]) { + throw new Error(`Object of type ${key} is not defined in container`); + } + testArgs[key] = container.support(key); + } + + return testArgs; +}; + +module.exports.getInjectedArguments = getInjectedArguments; diff --git a/lib/mocha/scenario.js b/lib/mocha/scenario.js new file mode 100644 index 000000000..fe4dea337 --- /dev/null +++ b/lib/mocha/scenario.js @@ -0,0 +1,204 @@ +const promiseRetry = require('promise-retry'); +const event = require('../event'); +const recorder = require('../recorder'); +const assertThrown = require('../assert/throws'); +const { ucfirst, isAsyncFunction } = require('../utils'); +const { getInjectedArguments } = require('./inject'); +const { fireHook } = require('./hooks'); + +const injectHook = function (inject, suite) { + try { + inject(); + } catch (err) { + recorder.throw(err); + } + recorder.catch(err => { + event.emit(event.test.failed, suite, err); + throw err; + }); + return recorder.promise(); +}; + +function makeDoneCallableOnce(done) { + let called = false; + return function (err) { + if (called) { + return; + } + called = true; + return done(err); + }; +} +/** + * Wraps test function, injects support objects from container, + * starts promise chain with recorder, performs before/after hooks + * through event system. + */ +module.exports.test = test => { + const testFn = test.fn; + if (!testFn) { + return test; + } + + test.steps = []; + test.timeout(0); + test.async = true; + + test.fn = function (done) { + const doneFn = makeDoneCallableOnce(done); + recorder.errHandler(err => { + recorder.session.start('teardown'); + recorder.cleanAsyncErr(); + if (test.throws) { + // check that test should actually fail + try { + assertThrown(err, test.throws); + event.emit(event.test.passed, test); + event.emit(event.test.finished, test); + recorder.add(doneFn); + return; + } catch (newErr) { + err = newErr; + } + } + event.emit(event.test.failed, test, err); + event.emit(event.test.finished, test); + recorder.add(() => doneFn(err)); + }); + + if (isAsyncFunction(testFn)) { + event.emit(event.test.started, test); + testFn + .call(test, getInjectedArguments(testFn, test)) + .then(() => { + recorder.add('fire test.passed', () => { + event.emit(event.test.passed, test); + event.emit(event.test.finished, test); + }); + recorder.add('finish test', doneFn); + }) + .catch(err => { + recorder.throw(err); + }) + .finally(() => { + recorder.catch(); + }); + return; + } + + try { + event.emit(event.test.started, test); + testFn.call(test, getInjectedArguments(testFn, test)); + } catch (err) { + recorder.throw(err); + } finally { + recorder.add('fire test.passed', () => { + event.emit(event.test.passed, test); + event.emit(event.test.finished, test); + }); + recorder.add('finish test', doneFn); + recorder.catch(); + } + }; + return test; +}; + +/** + * Injects arguments to function from controller + */ +module.exports.injected = function (fn, suite, hookName) { + return function (done) { + const doneFn = makeDoneCallableOnce(done); + const errHandler = err => { + recorder.session.start('teardown'); + recorder.cleanAsyncErr(); + event.emit(event.test.failed, suite, err); + if (hookName === 'after') event.emit(event.test.after, suite); + if (hookName === 'afterSuite') event.emit(event.suite.after, suite); + recorder.add(() => doneFn(err)); + }; + + recorder.errHandler(err => { + errHandler(err); + }); + + if (!fn) throw new Error('fn is not defined'); + + fireHook(event.hook.started, suite); + + this.test.body = fn.toString(); + + if (!recorder.isRunning()) { + recorder.errHandler(err => { + errHandler(err); + }); + } + + const opts = suite.opts || {}; + const retries = opts[`retry${ucfirst(hookName)}`] || 0; + + promiseRetry( + async (retry, number) => { + try { + recorder.startUnlessRunning(); + await fn.call(this, getInjectedArguments(fn)); + await recorder.promise().catch(err => retry(err)); + } catch (err) { + retry(err); + } finally { + if (number < retries) { + recorder.stop(); + recorder.start(); + } + } + }, + { retries }, + ) + .then(() => { + recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite)); + recorder.add(`finish ${hookName} hook`, doneFn); + recorder.catch(); + }) + .catch(e => { + recorder.throw(e); + recorder.catch(e => { + const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr(); + errHandler(err); + }); + recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e)); + }); + }; +}; + +/** + * Starts promise chain, so helpers could enqueue their hooks + */ +module.exports.setup = function (suite) { + return injectHook(() => { + recorder.startUnlessRunning(); + event.emit(event.test.before, suite && suite.ctx && suite.ctx.currentTest); + }, suite); +}; + +module.exports.teardown = function (suite) { + return injectHook(() => { + recorder.startUnlessRunning(); + event.emit(event.test.after, suite && suite.ctx && suite.ctx.currentTest); + }, suite); +}; + +module.exports.suiteSetup = function (suite) { + return injectHook(() => { + recorder.startUnlessRunning(); + event.emit(event.suite.before, suite); + }, suite); +}; + +module.exports.suiteTeardown = function (suite) { + return injectHook(() => { + recorder.startUnlessRunning(); + event.emit(event.suite.after, suite); + }, suite); +}; + +module.exports.getInjectedArguments = getInjectedArguments; diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js new file mode 100644 index 000000000..26a526c47 --- /dev/null +++ b/lib/mocha/scenarioConfig.js @@ -0,0 +1,92 @@ +/** + * Configuration for a test + * Can inject values and add custom configuration. + */ +class ScenarioConfig { + constructor(test) { + this.test = test; + } + + /** + * Declares that test throws error. + * Can pass an Error object or regex matching expected message. + * + * @param {*} err + */ + throws(err) { + this.test.throws = err; + return this; + } + + /** + * Declares that test should fail. + * If test passes - throws an error. + * Can pass an Error object or regex matching expected message. + * + * @param {*} err + */ + fails() { + this.test.throws = new Error(); + return this; + } + + /** + * Retry this test for number of times + * + * @param {number} retries + */ + retry(retries) { + this.test.retries(retries); + return this; + } + + /** + * Set timeout for this test + * @param {number} timeout + */ + timeout(timeout) { + this.test.timeout(timeout); + return this; + } + + /** + * Pass in additional objects to inject into test + * @param {*} obj + */ + inject(obj) { + this.test.inject = obj; + return this; + } + + /** + * Configures a helper. + * Helper name can be omitted and values will be applied to first helper. + */ + config(helper, obj) { + if (!obj) { + obj = helper; + helper = 0; + } + if (typeof obj === 'function') { + obj = obj(this.test); + } + if (!this.test.config) { + this.test.config = {}; + } + this.test.config[helper] = obj; + return this; + } + + /** + * Append a tag name to scenario title + * @param {string} tagName + */ + tag(tagName) { + if (tagName[0] !== '@') tagName = `@${tagName}`; + if (!this.test.tags) this.test.tags = []; + this.test.tags.push(tagName); + return this; + } +} + +module.exports = ScenarioConfig; diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js new file mode 100644 index 000000000..cde54cdf1 --- /dev/null +++ b/lib/mocha/ui.js @@ -0,0 +1,236 @@ +const escapeRe = require('escape-string-regexp'); +const Suite = require('mocha/lib/suite'); +const Test = require('mocha/lib/test'); + +const scenario = require('./scenario'); +const ScenarioConfig = require('./scenarioConfig'); +const FeatureConfig = require('./featureConfig'); +const addDataContext = require('../data/context'); +const container = require('../container'); + +const setContextTranslation = context => { + const contexts = container.translation().value('contexts'); + + if (contexts) { + for (const key of Object.keys(contexts)) { + if (context[key]) { + context[contexts[key]] = context[key]; + } + } + } +}; + +/** + * Codecept-style interface: + * + * Feature('login'); + * + * Scenario('login as regular user', ({I}) { + * I.fillField(); + * I.click(); + * I.see('Hello, '+data.login); + * }); + * + * @param {Mocha.Suite} suite Root suite. + * @ignore + */ +module.exports = function (suite) { + const suites = [suite]; + suite.timeout(0); + let afterAllHooks; + let afterEachHooks; + let afterAllHooksAreLoaded; + let afterEachHooksAreLoaded; + + suite.on('pre-require', (context, file, mocha) => { + const common = require('mocha/lib/interfaces/common')(suites, context, mocha); + + const addScenario = function (title, opts = {}, fn) { + const suite = suites[0]; + + if (typeof opts === 'function' && !fn) { + fn = opts; + opts = {}; + } + if (suite.pending) { + fn = null; + } + const test = new Test(title, fn); + test.fullTitle = () => `${suite.title}: ${test.title}`; + + test.tags = (suite.tags || []).concat(title.match(/(\@[a-zA-Z0-9-_]+)/g) || []); // match tags from title + test.file = file; + if (!test.inject) { + test.inject = {}; + } + + suite.addTest(scenario.test(test)); + if (opts.retries) test.retries(opts.retries); + if (opts.timeout) test.totalTimeout = opts.timeout; + test.opts = opts; + + return new ScenarioConfig(test); + }; + + // create dispatcher + + context.BeforeAll = common.before; + context.AfterAll = common.after; + + context.run = mocha.options.delay && common.runWithSuite(suite); + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + * @global + * @param {string} title + * @param {Object} [opts] + * @returns {FeatureConfig} + */ + + context.Feature = function (title, opts) { + if (suites.length > 1) { + suites.shift(); + } + + afterAllHooks = []; + afterEachHooks = []; + afterAllHooksAreLoaded = false; + afterEachHooksAreLoaded = false; + + const suite = Suite.create(suites[0], title); + if (!opts) opts = {}; + suite.opts = opts; + suite.timeout(0); + + if (opts.retries) suite.retries(opts.retries); + if (opts.timeout) suite.totalTimeout = opts.timeout; + + suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || []; // match tags from title + suite.file = file; + suite.fullTitle = () => `${suite.title}:`; + suites.unshift(suite); + suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); + afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]); + + suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); + afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]); + + if (opts.skipInfo && opts.skipInfo.skipped) { + suite.pending = true; + suite.opts = { ...suite.opts, skipInfo: opts.skipInfo }; + } + + return new FeatureConfig(suite); + }; + + /** + * Pending test suite. + * @global + * @kind constant + * @type {CodeceptJS.IFeature} + */ + context.xFeature = context.Feature.skip = function (title, opts) { + const skipInfo = { + skipped: true, + message: 'Skipped due to "skip" on Feature.', + }; + return context.Feature(title, { ...opts, skipInfo }); + }; + + context.BeforeSuite = function (fn) { + suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')); + }; + + context.AfterSuite = function (fn) { + afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]); + }; + + context.Background = context.Before = function (fn) { + suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')); + }; + + context.After = function (fn) { + afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + * @ignore + */ + context.Scenario = addScenario; + /** + * Exclusive test-case. + * @ignore + */ + context.Scenario.only = function (title, opts, fn) { + const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`; + mocha.grep(new RegExp(reString)); + process.env.SCENARIO_ONLY = true; + return addScenario(title, opts, fn); + }; + + /** + * Pending test case. + * @global + * @kind constant + * @type {CodeceptJS.IScenario} + */ + context.xScenario = context.Scenario.skip = function (title, opts = {}, fn) { + if (typeof opts === 'function' && !fn) { + opts = {}; + } + + return context.Scenario(title, opts); + }; + + /** + * Pending test case with message: 'Test not implemented!'. + * @global + * @kind constant + * @type {CodeceptJS.IScenario} + */ + context.Scenario.todo = function (title, opts = {}, fn) { + if (typeof opts === 'function' && !fn) { + fn = opts; + opts = {}; + } + + const skipInfo = { + message: 'Test not implemented!', + description: fn ? fn.toString() : '', + }; + + return context.Scenario(title, { ...opts, skipInfo }); + }; + + /** + * For translation + */ + + setContextTranslation(context); + + addDataContext(context); + }); + + suite.on('post-require', () => { + /** + * load hooks from arrays to suite to prevent reordering + */ + if (!afterEachHooksAreLoaded && Array.isArray(afterEachHooks)) { + afterEachHooks.forEach(hook => { + suites[0].afterEach(hook[0], hook[1]); + }); + afterEachHooksAreLoaded = true; + } + + if (!afterAllHooksAreLoaded && Array.isArray(afterAllHooks)) { + afterAllHooks.forEach(hook => { + suites[0].afterAll(hook[0], hook[1]); + }); + afterAllHooksAreLoaded = true; + } + }); +}; diff --git a/lib/output.js b/lib/output.js index 81df5dcf3..17f4eca94 100644 --- a/lib/output.js +++ b/lib/output.js @@ -174,6 +174,7 @@ module.exports = { */ started(test) { + if (outputLevel < 1) return; print(` ${colors.dim.bold('Scenario()')}`); }, @@ -195,12 +196,15 @@ module.exports = { hook: { started(hook) { + if (outputLevel < 1) return; print(` ${colors.dim.bold(hook.toCode())}`); }, passed(hook) { + if (outputLevel < 1) return; print(); }, failed(hook) { + if (outputLevel < 1) return; print(` ${colors.red.bold(hook.toCode())}`); }, }, From 7a513dc7ba38087a48bab4ab5b0e9115b5adc454 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 05:24:14 +0200 Subject: [PATCH 03/13] fixed tests --- lib/mocha/hooks.js | 4 ++++ lib/mocha/scenarioConfig.js | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index 48dad37d9..8490752a3 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -17,6 +17,10 @@ class Hook { return this.toString() + '()'; } + get title() { + return this.ctx?.test?.title || this.name; + } + get name() { return this.constructor.name; } diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index 26a526c47..80eab167d 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -87,6 +87,18 @@ class ScenarioConfig { this.test.tags.push(tagName); return this; } + + /** + * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection + * @param {Object} dependencies + * @returns {this} + */ + injectDependencies(dependencies) { + Object.keys(dependencies).forEach(key => { + this.test.inject[key] = dependencies[key]; + }); + return this; + } } module.exports = ScenarioConfig; From 304244b3c58707d218f2ba784bdfd74dd405f314 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sat, 4 Jan 2025 04:28:47 +0200 Subject: [PATCH 04/13] fixed formatting --- lib/mocha/bdd.js | 74 +++++------ lib/mocha/cli.js | 246 ++++++++++++++++++------------------ lib/mocha/factory.js | 102 +++++++-------- lib/mocha/featureConfig.js | 32 ++--- lib/mocha/gherkin.js | 220 ++++++++++++++++---------------- lib/mocha/hooks.js | 42 +++--- lib/mocha/scenario.js | 216 +++++++++++++++---------------- lib/mocha/scenarioConfig.js | 50 ++++---- lib/mocha/ui.js | 190 ++++++++++++++-------------- 9 files changed, 586 insertions(+), 586 deletions(-) diff --git a/lib/mocha/bdd.js b/lib/mocha/bdd.js index c320d3af6..d5dd15dd7 100644 --- a/lib/mocha/bdd.js +++ b/lib/mocha/bdd.js @@ -1,73 +1,73 @@ -const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('@cucumber/cucumber-expressions'); -const Config = require('../config'); +const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('@cucumber/cucumber-expressions') +const Config = require('../config') -let steps = {}; +let steps = {} -const STACK_POSITION = 2; +const STACK_POSITION = 2 /** * @param {*} step * @param {*} fn */ const addStep = (step, fn) => { - const avoidDuplicateSteps = Config.get('gherkin', {}).avoidDuplicateSteps || false; - const stack = new Error().stack; + const avoidDuplicateSteps = Config.get('gherkin', {}).avoidDuplicateSteps || false + const stack = new Error().stack if (avoidDuplicateSteps && steps[step]) { - throw new Error(`Step '${step}' is already defined`); + throw new Error(`Step '${step}' is already defined`) } - steps[step] = fn; - fn.line = stack && stack.split('\n')[STACK_POSITION]; + steps[step] = fn + fn.line = stack && stack.split('\n')[STACK_POSITION] if (fn.line) { fn.line = fn.line .trim() .replace(/^at (.*?)\(/, '(') - .replace(codecept_dir, '.'); + .replace(codecept_dir, '.') } -}; +} -const parameterTypeRegistry = new ParameterTypeRegistry(); +const parameterTypeRegistry = new ParameterTypeRegistry() const matchStep = step => { for (const stepName in steps) { if (stepName.indexOf('/') === 0) { - const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || []; - const res = step.match(new RegExp(regExpArr[1], regExpArr[2])); + const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || [] + const res = step.match(new RegExp(regExpArr[1], regExpArr[2])) if (res) { - const fn = steps[stepName]; - fn.params = res.slice(1); - return fn; + const fn = steps[stepName] + fn.params = res.slice(1) + return fn } - continue; + continue } - const expression = new CucumberExpression(stepName, parameterTypeRegistry); - const res = expression.match(step); + const expression = new CucumberExpression(stepName, parameterTypeRegistry) + const res = expression.match(step) if (res) { - const fn = steps[stepName]; - fn.params = res.map(arg => arg.getValue()); - return fn; + const fn = steps[stepName] + fn.params = res.map(arg => arg.getValue()) + return fn } } - throw new Error(`No steps matching "${step.toString()}"`); -}; + throw new Error(`No steps matching "${step.toString()}"`) +} const clearSteps = () => { - steps = {}; -}; + steps = {} +} const getSteps = () => { - return steps; -}; + return steps +} const defineParameterType = options => { - const parameterType = buildParameterType(options); - parameterTypeRegistry.defineParameterType(parameterType); -}; + const parameterType = buildParameterType(options) + parameterTypeRegistry.defineParameterType(parameterType) +} const buildParameterType = ({ name, regexp, transformer, useForSnippets, preferForRegexpMatch }) => { - if (typeof useForSnippets !== 'boolean') useForSnippets = true; - if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false; - return new ParameterType(name, regexp, null, transformer, useForSnippets, preferForRegexpMatch); -}; + if (typeof useForSnippets !== 'boolean') useForSnippets = true + if (typeof preferForRegexpMatch !== 'boolean') preferForRegexpMatch = false + return new ParameterType(name, regexp, null, transformer, useForSnippets, preferForRegexpMatch) +} module.exports = { Given: addStep, @@ -78,4 +78,4 @@ module.exports = { getSteps, clearSteps, defineParameterType, -}; +} diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js index ec132d6e3..5e1328c75 100644 --- a/lib/mocha/cli.js +++ b/lib/mocha/cli.js @@ -1,257 +1,257 @@ const { reporters: { Base }, -} = require('mocha'); -const ms = require('ms'); -const event = require('../event'); -const AssertionFailedError = require('../assert/error'); -const output = require('../output'); +} = require('mocha') +const ms = require('ms') +const event = require('../event') +const AssertionFailedError = require('../assert/error') +const output = require('../output') -const cursor = Base.cursor; -let currentMetaStep = []; -let codeceptjsEventDispatchersRegistered = false; +const cursor = Base.cursor +let currentMetaStep = [] +let codeceptjsEventDispatchersRegistered = false class Cli extends Base { constructor(runner, opts) { - super(runner); - let level = 0; - this.loadedTests = []; - opts = opts.reporterOptions || opts; - if (opts.steps) level = 1; - if (opts.debug) level = 2; - if (opts.verbose) level = 3; - output.level(level); - output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`); - output.print(`Using test root "${global.codecept_dir}"`); - - const showSteps = level >= 1; + super(runner) + let level = 0 + this.loadedTests = [] + opts = opts.reporterOptions || opts + if (opts.steps) level = 1 + if (opts.debug) level = 2 + if (opts.verbose) level = 3 + output.level(level) + output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`) + output.print(`Using test root "${global.codecept_dir}"`) + + const showSteps = level >= 1 if (level >= 2) { - const Containter = require('../container'); - output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)); - output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)); + const Containter = require('../container') + output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)) + output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)) } runner.on('start', () => { - console.log(); - }); + console.log() + }) runner.on('suite', suite => { - output.suite.started(suite); - }); + output.suite.started(suite) + }) runner.on('fail', test => { if (test.ctx.currentTest) { - this.loadedTests.push(test.ctx.currentTest.uid); + this.loadedTests.push(test.ctx.currentTest.uid) } if (showSteps && test.steps) { - return output.scenario.failed(test); + return output.scenario.failed(test) } - cursor.CR(); - output.test.failed(test); - }); + cursor.CR() + output.test.failed(test) + }) runner.on('pending', test => { if (test.parent && test.parent.pending) { - const suite = test.parent; - const skipInfo = suite.opts.skipInfo || {}; - skipTestConfig(test, skipInfo.message); + const suite = test.parent + const skipInfo = suite.opts.skipInfo || {} + skipTestConfig(test, skipInfo.message) } else { - skipTestConfig(test, null); + skipTestConfig(test, null) } - this.loadedTests.push(test.uid); - cursor.CR(); - output.test.skipped(test); - }); + this.loadedTests.push(test.uid) + cursor.CR() + output.test.skipped(test) + }) runner.on('pass', test => { if (showSteps && test.steps) { - return output.scenario.passed(test); + return output.scenario.passed(test) } - cursor.CR(); - output.test.passed(test); - }); + cursor.CR() + output.test.passed(test) + }) if (showSteps) { runner.on('test', test => { - currentMetaStep = []; + currentMetaStep = [] if (test.steps) { - output.test.started(test); + output.test.started(test) } - }); + }) if (!codeceptjsEventDispatchersRegistered) { - codeceptjsEventDispatchersRegistered = true; + codeceptjsEventDispatchersRegistered = true event.dispatcher.on(event.bddStep.started, step => { - output.stepShift = 2; - output.step(step); - }); + output.stepShift = 2 + output.step(step) + }) event.dispatcher.on(event.step.started, step => { - let processingStep = step; - const metaSteps = []; + let processingStep = step + const metaSteps = [] while (processingStep.metaStep) { - metaSteps.unshift(processingStep.metaStep); - processingStep = processingStep.metaStep; + metaSteps.unshift(processingStep.metaStep) + processingStep = processingStep.metaStep } - const shift = metaSteps.length; + const shift = metaSteps.length for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) { if (currentMetaStep[i] !== metaSteps[i]) { - output.stepShift = 3 + 2 * i; - if (!metaSteps[i]) continue; + output.stepShift = 3 + 2 * i + if (!metaSteps[i]) continue // bdd steps are handled by bddStep.started - if (metaSteps[i].isBDD()) continue; - output.step(metaSteps[i]); + if (metaSteps[i].isBDD()) continue + output.step(metaSteps[i]) } } - currentMetaStep = metaSteps; - output.stepShift = 3 + 2 * shift; + currentMetaStep = metaSteps + output.stepShift = 3 + 2 * shift if (step.helper.constructor.name !== 'ExpectHelper') { - output.step(step); + output.step(step) } - }); + }) event.dispatcher.on(event.step.finished, () => { - output.stepShift = 0; - }); + output.stepShift = 0 + }) } } runner.on('suite end', suite => { - let skippedCount = 0; - const grep = runner._grep; + let skippedCount = 0 + const grep = runner._grep for (const test of suite.tests) { if (!test.state && !this.loadedTests.includes(test.uid)) { if (matchTest(grep, test.title)) { if (!test.opts) { - test.opts = {}; + test.opts = {} } if (!test.opts.skipInfo) { - test.opts.skipInfo = {}; + test.opts.skipInfo = {} } - skipTestConfig(test, "Skipped due to failure in 'before' hook"); - output.test.skipped(test); - skippedCount += 1; + skipTestConfig(test, "Skipped due to failure in 'before' hook") + output.test.skipped(test) + skippedCount += 1 } } } - this.stats.pending += skippedCount; - this.stats.tests += skippedCount; - }); + this.stats.pending += skippedCount + this.stats.tests += skippedCount + }) - runner.on('end', this.result.bind(this)); + runner.on('end', this.result.bind(this)) } result() { - const stats = this.stats; - stats.failedHooks = 0; - console.log(); + const stats = this.stats + stats.failedHooks = 0 + console.log() // passes if (stats.failures) { - output.print(output.styles.bold('-- FAILURES:')); + output.print(output.styles.bold('-- FAILURES:')) } - const failuresLog = []; + const failuresLog = [] // failures if (stats.failures) { // append step traces this.failures.map(test => { - const err = test.err; + const err = test.err - let log = ''; + let log = '' if (err instanceof AssertionFailedError) { - err.message = err.inspect(); + err.message = err.inspect() } - const steps = test.steps || (test.ctx && test.ctx.test.steps); + const steps = test.steps || (test.ctx && test.ctx.test.steps) if (steps && steps.length) { - let scenarioTrace = ''; + let scenarioTrace = '' steps.reverse().forEach(step => { - const line = `- ${step.toCode()} ${step.line()}`; + const line = `- ${step.toCode()} ${step.line()}` // if (step.status === 'failed') line = '' + line; - scenarioTrace += `\n${line}`; - }); - log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; + scenarioTrace += `\n${line}` + }) + log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n` } // display artifacts in debug mode if (test?.artifacts && Object.keys(test.artifacts).length) { - log += `\n${output.styles.bold('Artifacts:')}`; + log += `\n${output.styles.bold('Artifacts:')}` for (const artifact of Object.keys(test.artifacts)) { - log += `\n- ${artifact}: ${test.artifacts[artifact]}`; + log += `\n- ${artifact}: ${test.artifacts[artifact]}` } } try { - let stack = err.stack ? err.stack.split('\n') : []; + let stack = err.stack ? err.stack.split('\n') : [] if (stack[0] && stack[0].includes(err.message)) { - stack.shift(); + stack.shift() } if (output.level() < 3) { - stack = stack.slice(0, 3); + stack = stack.slice(0, 3) } - err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; + err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}` // clone err object so stack trace adjustments won't affect test other reports - test.err = err; - return test; + test.err = err + return test } catch (e) { - throw Error(e); + throw Error(e) } - }); + }) - const originalLog = Base.consoleLog; + const originalLog = Base.consoleLog Base.consoleLog = (...data) => { - failuresLog.push([...data]); - originalLog(...data); - }; - Base.list(this.failures); - Base.consoleLog = originalLog; - console.log(); + failuresLog.push([...data]) + originalLog(...data) + } + Base.list(this.failures) + Base.consoleLog = originalLog + console.log() } this.failures.forEach(failure => { if (failure.constructor.name === 'Hook') { - stats.failedHooks += 1; + stats.failedHooks += 1 } - }); - event.emit(event.all.failures, { failuresLog, stats }); - output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks); + }) + event.emit(event.all.failures, { failuresLog, stats }) + output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks) if (stats.failures && output.level() < 3) { - output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); + output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')) } } } function matchTest(grep, test) { if (grep) { - return grep.test(test); + return grep.test(test) } - return true; + return true } function skipTestConfig(test, message) { if (!test.opts) { - test.opts = {}; + test.opts = {} } if (!test.opts.skipInfo) { - test.opts.skipInfo = {}; + test.opts.skipInfo = {} } - test.opts.skipInfo.message = test.opts.skipInfo.message || message; - test.opts.skipInfo.isFastSkipped = true; - event.emit(event.test.skipped, test); - test.state = 'skipped'; + test.opts.skipInfo.message = test.opts.skipInfo.message || message + test.opts.skipInfo.isFastSkipped = true + event.emit(event.test.skipped, test) + test.state = 'skipped' } module.exports = function (runner, opts) { - return new Cli(runner, opts); -}; + return new Cli(runner, opts) +} diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js index 07fc70368..d5ef1444e 100644 --- a/lib/mocha/factory.js +++ b/lib/mocha/factory.js @@ -1,107 +1,107 @@ -const Mocha = require('mocha'); -const fsPath = require('path'); -const fs = require('fs'); -const reporter = require('./cli'); -const gherkinParser = require('./gherkin'); -const output = require('../output'); -const { genTestId } = require('../utils'); -const ConnectionRefused = require('../helper/errors/ConnectionRefused'); +const Mocha = require('mocha') +const fsPath = require('path') +const fs = require('fs') +const reporter = require('./cli') +const gherkinParser = require('./gherkin') +const output = require('../output') +const { genTestId } = require('../utils') +const ConnectionRefused = require('../helper/errors/ConnectionRefused') -const scenarioUi = fsPath.join(__dirname, './ui.js'); +const scenarioUi = fsPath.join(__dirname, './ui.js') -let mocha; +let mocha class MochaFactory { static create(config, opts) { - mocha = new Mocha(Object.assign(config, opts)); - output.process(opts.child); - mocha.ui(scenarioUi); + mocha = new Mocha(Object.assign(config, opts)) + output.process(opts.child) + mocha.ui(scenarioUi) Mocha.Runner.prototype.uncaught = function (err) { if (err) { if (err.toString().indexOf('ECONNREFUSED') >= 0) { - err = new ConnectionRefused(err); + err = new ConnectionRefused(err) } - output.error(err); - output.print(err.stack); - process.exit(1); + output.error(err) + output.print(err.stack) + process.exit(1) } - output.error('Uncaught undefined exception'); - process.exit(1); - }; + output.error('Uncaught undefined exception') + process.exit(1) + } mocha.loadFiles = fn => { // load features if (mocha.suite.suites.length === 0) { - mocha.files.filter(file => file.match(/\.feature$/)).forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file))); + mocha.files.filter(file => file.match(/\.feature$/)).forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file))) // remove feature files - mocha.files = mocha.files.filter(file => !file.match(/\.feature$/)); + mocha.files = mocha.files.filter(file => !file.match(/\.feature$/)) - Mocha.prototype.loadFiles.call(mocha, fn); + Mocha.prototype.loadFiles.call(mocha, fn) // add ids for each test and check uniqueness - const dupes = []; - let missingFeatureInFile = []; - const seenTests = []; + const dupes = [] + let missingFeatureInFile = [] + const seenTests = [] mocha.suite.eachTest(test => { - test.uid = genTestId(test); + test.uid = genTestId(test) - const name = test.fullTitle(); + const name = test.fullTitle() if (seenTests.includes(test.uid)) { - dupes.push(name); + dupes.push(name) } - seenTests.push(test.uid); + seenTests.push(test.uid) if (name.slice(0, name.indexOf(':')) === '') { - missingFeatureInFile.push(test.file); + missingFeatureInFile.push(test.file) } - }); + }) if (dupes.length) { // ideally this should be no-op and throw (breaking change)... - output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`); + output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`) } if (missingFeatureInFile.length) { - missingFeatureInFile = [...new Set(missingFeatureInFile)]; - output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`); + missingFeatureInFile = [...new Set(missingFeatureInFile)] + output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`) } } - }; + } - const presetReporter = opts.reporter || config.reporter; + const presetReporter = opts.reporter || config.reporter // use standard reporter if (!presetReporter) { - mocha.reporter(reporter, opts); - return mocha; + mocha.reporter(reporter, opts) + return mocha } // load custom reporter with options - const reporterOptions = Object.assign(config.reporterOptions || {}); + const reporterOptions = Object.assign(config.reporterOptions || {}) if (opts.reporterOptions !== undefined) { opts.reporterOptions.split(',').forEach(opt => { - const L = opt.split('='); + const L = opt.split('=') if (L.length > 2 || L.length === 0) { - throw new Error(`invalid reporter option '${opt}'`); + throw new Error(`invalid reporter option '${opt}'`) } else if (L.length === 2) { - reporterOptions[L[0]] = L[1]; + reporterOptions[L[0]] = L[1] } else { - reporterOptions[L[0]] = true; + reporterOptions[L[0]] = true } - }); + }) } - const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter'); + const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter') if (reporterOptions['codeceptjs-cli-reporter'] && attributes) { - Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes); - delete reporterOptions['codeceptjs-cli-reporter']; + Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes) + delete reporterOptions['codeceptjs-cli-reporter'] } // custom reporters - mocha.reporter(presetReporter, reporterOptions); - return mocha; + mocha.reporter(presetReporter, reporterOptions) + return mocha } } -module.exports = MochaFactory; +module.exports = MochaFactory diff --git a/lib/mocha/featureConfig.js b/lib/mocha/featureConfig.js index c87ebe187..7c3531a99 100644 --- a/lib/mocha/featureConfig.js +++ b/lib/mocha/featureConfig.js @@ -4,7 +4,7 @@ */ class FeatureConfig { constructor(suite) { - this.suite = suite; + this.suite = suite } /** @@ -14,8 +14,8 @@ class FeatureConfig { * @returns {this} */ retry(retries) { - this.suite.retries(retries); - return this; + this.suite.retries(retries) + return this } /** @@ -24,8 +24,8 @@ class FeatureConfig { * @returns {this} */ timeout(timeout) { - this.suite.timeout(timeout); - return this; + this.suite.timeout(timeout) + return this } /** @@ -34,17 +34,17 @@ class FeatureConfig { */ config(helper, obj) { if (!obj) { - obj = helper; - helper = 0; + obj = helper + helper = 0 } if (typeof obj === 'function') { - obj = obj(this.suite); + obj = obj(this.suite) } if (!this.suite.config) { - this.suite.config = {}; + this.suite.config = {} } - this.suite.config[helper] = obj; - return this; + this.suite.config[helper] = obj + return this } /** @@ -53,11 +53,11 @@ class FeatureConfig { * @returns {this} */ tag(tagName) { - if (tagName[0] !== '@') tagName = `@${tagName}`; - if (!this.suite.tags) this.suite.tags = []; - this.suite.tags.push(tagName); - return this; + if (tagName[0] !== '@') tagName = `@${tagName}` + if (!this.suite.tags) this.suite.tags = [] + this.suite.tags.push(tagName) + return this } } -module.exports = FeatureConfig; +module.exports = FeatureConfig diff --git a/lib/mocha/gherkin.js b/lib/mocha/gherkin.js index 200db3834..10b5d3b75 100644 --- a/lib/mocha/gherkin.js +++ b/lib/mocha/gherkin.js @@ -1,196 +1,196 @@ -const Gherkin = require('@cucumber/gherkin'); -const Messages = require('@cucumber/messages'); -const { Context, Suite, Test } = require('mocha'); -const debug = require('debug')('codeceptjs:bdd'); - -const { matchStep } = require('./bdd'); -const event = require('../event'); -const scenario = require('./scenario'); -const Step = require('../step'); -const DataTableArgument = require('../data/dataTableArgument'); -const transform = require('../transform'); - -const uuidFn = Messages.IdGenerator.uuid(); -const builder = new Gherkin.AstBuilder(uuidFn); -const matcher = new Gherkin.GherkinClassicTokenMatcher(); -const parser = new Gherkin.Parser(builder, matcher); -parser.stopAtFirstError = false; +const Gherkin = require('@cucumber/gherkin') +const Messages = require('@cucumber/messages') +const { Context, Suite, Test } = require('mocha') +const debug = require('debug')('codeceptjs:bdd') + +const { matchStep } = require('./bdd') +const event = require('../event') +const scenario = require('./scenario') +const Step = require('../step') +const DataTableArgument = require('../data/dataTableArgument') +const transform = require('../transform') + +const uuidFn = Messages.IdGenerator.uuid() +const builder = new Gherkin.AstBuilder(uuidFn) +const matcher = new Gherkin.GherkinClassicTokenMatcher() +const parser = new Gherkin.Parser(builder, matcher) +parser.stopAtFirstError = false module.exports = (text, file) => { - const ast = parser.parse(text); - let currentLanguage; + const ast = parser.parse(text) + let currentLanguage if (ast.feature) { - currentLanguage = getTranslation(ast.feature.language); + currentLanguage = getTranslation(ast.feature.language) } if (!ast.feature) { - throw new Error(`No 'Features' available in Gherkin '${file}' provided!`); + throw new Error(`No 'Features' available in Gherkin '${file}' provided!`) } - const suite = new Suite(ast.feature.name, new Context()); - const tags = ast.feature.tags.map(t => t.name); - suite.title = `${suite.title} ${tags.join(' ')}`.trim(); - suite.tags = tags || []; - suite.comment = ast.feature.description; - suite.feature = ast.feature; - suite.file = file; - suite.timeout(0); - - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); - suite.afterEach('codeceptjs.after', () => scenario.teardown(suite)); - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); - suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)); + const suite = new Suite(ast.feature.name, new Context()) + const tags = ast.feature.tags.map(t => t.name) + suite.title = `${suite.title} ${tags.join(' ')}`.trim() + suite.tags = tags || [] + suite.comment = ast.feature.description + suite.feature = ast.feature + suite.file = file + suite.timeout(0) + + suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) + suite.afterEach('codeceptjs.after', () => scenario.teardown(suite)) + suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) + suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)) const runSteps = async steps => { for (const step of steps) { - const metaStep = new Step.MetaStep(null, step.text); - metaStep.actor = step.keyword.trim(); - let helperStep; + const metaStep = new Step.MetaStep(null, step.text) + metaStep.actor = step.keyword.trim() + let helperStep const setMetaStep = step => { - helperStep = step; + helperStep = step if (step.metaStep) { if (step.metaStep === metaStep) { - return; + return } - setMetaStep(step.metaStep); - return; + setMetaStep(step.metaStep) + return } - step.metaStep = metaStep; - }; - const fn = matchStep(step.text); + step.metaStep = metaStep + } + const fn = matchStep(step.text) if (step.dataTable) { fn.params.push({ ...step.dataTable, parse: () => new DataTableArgument(step.dataTable), - }); - metaStep.comment = `\n${transformTable(step.dataTable)}`; + }) + metaStep.comment = `\n${transformTable(step.dataTable)}` } if (step.docString) { - fn.params.push(step.docString); - metaStep.comment = `\n"""\n${step.docString.content}\n"""`; + fn.params.push(step.docString) + metaStep.comment = `\n"""\n${step.docString.content}\n"""` } - step.startTime = Date.now(); - step.match = fn.line; - event.emit(event.bddStep.before, step); - event.emit(event.bddStep.started, metaStep); - event.dispatcher.prependListener(event.step.before, setMetaStep); + step.startTime = Date.now() + step.match = fn.line + event.emit(event.bddStep.before, step) + event.emit(event.bddStep.started, metaStep) + event.dispatcher.prependListener(event.step.before, setMetaStep) try { - debug(`Step '${step.text}' started...`); - await fn(...fn.params); - debug('Step passed'); - step.status = 'passed'; + debug(`Step '${step.text}' started...`) + await fn(...fn.params) + debug('Step passed') + step.status = 'passed' } catch (err) { - debug(`Step failed: ${err?.message}`); - step.status = 'failed'; - step.err = err; - throw err; + debug(`Step failed: ${err?.message}`) + step.status = 'failed' + step.err = err + throw err } finally { - step.endTime = Date.now(); - event.dispatcher.removeListener(event.step.before, setMetaStep); + step.endTime = Date.now() + event.dispatcher.removeListener(event.step.before, setMetaStep) } - event.emit(event.bddStep.finished, metaStep); - event.emit(event.bddStep.after, step); + event.emit(event.bddStep.finished, metaStep) + event.emit(event.bddStep.after, step) } - }; + } for (const child of ast.feature.children) { if (child.background) { suite.beforeEach( 'Before', scenario.injected(async () => runSteps(child.background.steps), suite, 'before'), - ); - continue; + ) + continue } if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) { for (const examples of child.scenario.examples) { - const fields = examples.tableHeader.cells.map(c => c.value); + const fields = examples.tableHeader.cells.map(c => c.value) for (const example of examples.tableBody) { - let exampleSteps = [...child.scenario.steps]; - const current = {}; + let exampleSteps = [...child.scenario.steps] + const current = {} for (const index in example.cells) { - const placeholder = fields[index]; - const value = transform('gherkin.examples', example.cells[index].value); - example.cells[index].value = value; - current[placeholder] = value; + const placeholder = fields[index] + const value = transform('gherkin.examples', example.cells[index].value) + example.cells[index].value = value + current[placeholder] = value exampleSteps = exampleSteps.map(step => { - step = { ...step }; - step.text = step.text.split(`<${placeholder}>`).join(value); - return step; - }); + step = { ...step } + step.text = step.text.split(`<${placeholder}>`).join(value) + return step + }) } - const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name)); - let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim(); + const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name)) + let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim() for (const [key, value] of Object.entries(current)) { if (title.includes(`<${key}>`)) { - title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value); + title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value) } } - const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))); - test.tags = suite.tags.concat(tags); - test.file = file; - suite.addTest(scenario.test(test)); + const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))) + test.tags = suite.tags.concat(tags) + test.file = file + suite.addTest(scenario.test(test)) } } - continue; + continue } if (child.scenario) { - const tags = child.scenario.tags.map(t => t.name); - const title = `${child.scenario.name} ${tags.join(' ')}`.trim(); - const test = new Test(title, async () => runSteps(child.scenario.steps)); - test.tags = suite.tags.concat(tags); - test.file = file; - suite.addTest(scenario.test(test)); + const tags = child.scenario.tags.map(t => t.name) + const title = `${child.scenario.name} ${tags.join(' ')}`.trim() + const test = new Test(title, async () => runSteps(child.scenario.steps)) + test.tags = suite.tags.concat(tags) + test.file = file + suite.addTest(scenario.test(test)) } } - return suite; -}; + return suite +} function transformTable(table) { - let str = ''; + let str = '' for (const id in table.rows) { - const cells = table.rows[id].cells; + const cells = table.rows[id].cells str += cells .map(c => c.value) .map(c => c.padEnd(15)) - .join(' | '); - str += '\n'; + .join(' | ') + str += '\n' } - return str; + return str } function addExampleInTable(exampleSteps, placeholders) { - const steps = JSON.parse(JSON.stringify(exampleSteps)); + const steps = JSON.parse(JSON.stringify(exampleSteps)) for (const placeholder in placeholders) { steps.map(step => { - step = { ...step }; + step = { ...step } if (step.dataTable) { for (const id in step.dataTable.rows) { - const cells = step.dataTable.rows[id].cells; - cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder]))); + const cells = step.dataTable.rows[id].cells + cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder]))) } } - return step; - }); + return step + }) } - return steps; + return steps } function getTranslation(language) { - const translations = Object.keys(require('../../translations')); + const translations = Object.keys(require('../../translations')) for (const availableTranslation of translations) { if (!language) { - break; + break } if (availableTranslation.includes(language)) { - return require('../../translations')[availableTranslation]; + return require('../../translations')[availableTranslation] } } } diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index 8490752a3..dd3dbda26 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -1,28 +1,28 @@ -const event = require('../event'); +const event = require('../event') class Hook { constructor(context, error) { - this.suite = context.suite; - this.test = context.test; - this.runnable = context.ctx.test; - this.ctx = context.ctx; - this.error = error; + this.suite = context.suite + this.test = context.test + this.runnable = context.ctx.test + this.ctx = context.ctx + this.error = error } toString() { - return this.constructor.name.replace('Hook', ''); + return this.constructor.name.replace('Hook', '') } toCode() { - return this.toString() + '()'; + return this.toString() + '()' } get title() { - return this.ctx?.test?.title || this.name; + return this.ctx?.test?.title || this.name } get name() { - return this.constructor.name; + return this.constructor.name } } @@ -35,22 +35,22 @@ class BeforeSuiteHook extends Hook {} class AfterSuiteHook extends Hook {} function fireHook(eventType, suite, error) { - const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1]; + const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1] switch (hook) { case 'before each': - event.emit(eventType, new BeforeHook(suite)); - break; + event.emit(eventType, new BeforeHook(suite)) + break case 'after each': - event.emit(eventType, new AfterHook(suite, error)); - break; + event.emit(eventType, new AfterHook(suite, error)) + break case 'before all': - event.emit(eventType, new BeforeSuiteHook(suite)); - break; + event.emit(eventType, new BeforeSuiteHook(suite)) + break case 'after all': - event.emit(eventType, new AfterSuiteHook(suite, error)); - break; + event.emit(eventType, new AfterSuiteHook(suite, error)) + break default: - event.emit(eventType, suite, error); + event.emit(eventType, suite, error) } } @@ -60,4 +60,4 @@ module.exports = { BeforeSuiteHook, AfterSuiteHook, fireHook, -}; +} diff --git a/lib/mocha/scenario.js b/lib/mocha/scenario.js index fe4dea337..48b503263 100644 --- a/lib/mocha/scenario.js +++ b/lib/mocha/scenario.js @@ -1,33 +1,33 @@ -const promiseRetry = require('promise-retry'); -const event = require('../event'); -const recorder = require('../recorder'); -const assertThrown = require('../assert/throws'); -const { ucfirst, isAsyncFunction } = require('../utils'); -const { getInjectedArguments } = require('./inject'); -const { fireHook } = require('./hooks'); +const promiseRetry = require('promise-retry') +const event = require('../event') +const recorder = require('../recorder') +const assertThrown = require('../assert/throws') +const { ucfirst, isAsyncFunction } = require('../utils') +const { getInjectedArguments } = require('./inject') +const { fireHook } = require('./hooks') const injectHook = function (inject, suite) { try { - inject(); + inject() } catch (err) { - recorder.throw(err); + recorder.throw(err) } recorder.catch(err => { - event.emit(event.test.failed, suite, err); - throw err; - }); - return recorder.promise(); -}; + event.emit(event.test.failed, suite, err) + throw err + }) + return recorder.promise() +} function makeDoneCallableOnce(done) { - let called = false; + let called = false return function (err) { if (called) { - return; + return } - called = true; - return done(err); - }; + called = true + return done(err) + } } /** * Wraps test function, injects support objects from container, @@ -35,170 +35,170 @@ function makeDoneCallableOnce(done) { * through event system. */ module.exports.test = test => { - const testFn = test.fn; + const testFn = test.fn if (!testFn) { - return test; + return test } - test.steps = []; - test.timeout(0); - test.async = true; + test.steps = [] + test.timeout(0) + test.async = true test.fn = function (done) { - const doneFn = makeDoneCallableOnce(done); + const doneFn = makeDoneCallableOnce(done) recorder.errHandler(err => { - recorder.session.start('teardown'); - recorder.cleanAsyncErr(); + recorder.session.start('teardown') + recorder.cleanAsyncErr() if (test.throws) { // check that test should actually fail try { - assertThrown(err, test.throws); - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - recorder.add(doneFn); - return; + assertThrown(err, test.throws) + event.emit(event.test.passed, test) + event.emit(event.test.finished, test) + recorder.add(doneFn) + return } catch (newErr) { - err = newErr; + err = newErr } } - event.emit(event.test.failed, test, err); - event.emit(event.test.finished, test); - recorder.add(() => doneFn(err)); - }); + event.emit(event.test.failed, test, err) + event.emit(event.test.finished, test) + recorder.add(() => doneFn(err)) + }) if (isAsyncFunction(testFn)) { - event.emit(event.test.started, test); + event.emit(event.test.started, test) testFn .call(test, getInjectedArguments(testFn, test)) .then(() => { recorder.add('fire test.passed', () => { - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - }); - recorder.add('finish test', doneFn); + event.emit(event.test.passed, test) + event.emit(event.test.finished, test) + }) + recorder.add('finish test', doneFn) }) .catch(err => { - recorder.throw(err); + recorder.throw(err) }) .finally(() => { - recorder.catch(); - }); - return; + recorder.catch() + }) + return } try { - event.emit(event.test.started, test); - testFn.call(test, getInjectedArguments(testFn, test)); + event.emit(event.test.started, test) + testFn.call(test, getInjectedArguments(testFn, test)) } catch (err) { - recorder.throw(err); + recorder.throw(err) } finally { recorder.add('fire test.passed', () => { - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - }); - recorder.add('finish test', doneFn); - recorder.catch(); + event.emit(event.test.passed, test) + event.emit(event.test.finished, test) + }) + recorder.add('finish test', doneFn) + recorder.catch() } - }; - return test; -}; + } + return test +} /** * Injects arguments to function from controller */ module.exports.injected = function (fn, suite, hookName) { return function (done) { - const doneFn = makeDoneCallableOnce(done); + const doneFn = makeDoneCallableOnce(done) const errHandler = err => { - recorder.session.start('teardown'); - recorder.cleanAsyncErr(); - event.emit(event.test.failed, suite, err); - if (hookName === 'after') event.emit(event.test.after, suite); - if (hookName === 'afterSuite') event.emit(event.suite.after, suite); - recorder.add(() => doneFn(err)); - }; + recorder.session.start('teardown') + recorder.cleanAsyncErr() + event.emit(event.test.failed, suite, err) + if (hookName === 'after') event.emit(event.test.after, suite) + if (hookName === 'afterSuite') event.emit(event.suite.after, suite) + recorder.add(() => doneFn(err)) + } recorder.errHandler(err => { - errHandler(err); - }); + errHandler(err) + }) - if (!fn) throw new Error('fn is not defined'); + if (!fn) throw new Error('fn is not defined') - fireHook(event.hook.started, suite); + fireHook(event.hook.started, suite) - this.test.body = fn.toString(); + this.test.body = fn.toString() if (!recorder.isRunning()) { recorder.errHandler(err => { - errHandler(err); - }); + errHandler(err) + }) } - const opts = suite.opts || {}; - const retries = opts[`retry${ucfirst(hookName)}`] || 0; + const opts = suite.opts || {} + const retries = opts[`retry${ucfirst(hookName)}`] || 0 promiseRetry( async (retry, number) => { try { - recorder.startUnlessRunning(); - await fn.call(this, getInjectedArguments(fn)); - await recorder.promise().catch(err => retry(err)); + recorder.startUnlessRunning() + await fn.call(this, getInjectedArguments(fn)) + await recorder.promise().catch(err => retry(err)) } catch (err) { - retry(err); + retry(err) } finally { if (number < retries) { - recorder.stop(); - recorder.start(); + recorder.stop() + recorder.start() } } }, { retries }, ) .then(() => { - recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite)); - recorder.add(`finish ${hookName} hook`, doneFn); - recorder.catch(); + recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite)) + recorder.add(`finish ${hookName} hook`, doneFn) + recorder.catch() }) .catch(e => { - recorder.throw(e); + recorder.throw(e) recorder.catch(e => { - const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr(); - errHandler(err); - }); - recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e)); - }); - }; -}; + const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr() + errHandler(err) + }) + recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e)) + }) + } +} /** * Starts promise chain, so helpers could enqueue their hooks */ module.exports.setup = function (suite) { return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.test.before, suite && suite.ctx && suite.ctx.currentTest); - }, suite); -}; + recorder.startUnlessRunning() + event.emit(event.test.before, suite && suite.ctx && suite.ctx.currentTest) + }, suite) +} module.exports.teardown = function (suite) { return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.test.after, suite && suite.ctx && suite.ctx.currentTest); - }, suite); -}; + recorder.startUnlessRunning() + event.emit(event.test.after, suite && suite.ctx && suite.ctx.currentTest) + }, suite) +} module.exports.suiteSetup = function (suite) { return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.suite.before, suite); - }, suite); -}; + recorder.startUnlessRunning() + event.emit(event.suite.before, suite) + }, suite) +} module.exports.suiteTeardown = function (suite) { return injectHook(() => { - recorder.startUnlessRunning(); - event.emit(event.suite.after, suite); - }, suite); -}; + recorder.startUnlessRunning() + event.emit(event.suite.after, suite) + }, suite) +} -module.exports.getInjectedArguments = getInjectedArguments; +module.exports.getInjectedArguments = getInjectedArguments diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index 80eab167d..f5d82ed0d 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -4,7 +4,7 @@ */ class ScenarioConfig { constructor(test) { - this.test = test; + this.test = test } /** @@ -14,8 +14,8 @@ class ScenarioConfig { * @param {*} err */ throws(err) { - this.test.throws = err; - return this; + this.test.throws = err + return this } /** @@ -26,8 +26,8 @@ class ScenarioConfig { * @param {*} err */ fails() { - this.test.throws = new Error(); - return this; + this.test.throws = new Error() + return this } /** @@ -36,8 +36,8 @@ class ScenarioConfig { * @param {number} retries */ retry(retries) { - this.test.retries(retries); - return this; + this.test.retries(retries) + return this } /** @@ -45,8 +45,8 @@ class ScenarioConfig { * @param {number} timeout */ timeout(timeout) { - this.test.timeout(timeout); - return this; + this.test.timeout(timeout) + return this } /** @@ -54,8 +54,8 @@ class ScenarioConfig { * @param {*} obj */ inject(obj) { - this.test.inject = obj; - return this; + this.test.inject = obj + return this } /** @@ -64,17 +64,17 @@ class ScenarioConfig { */ config(helper, obj) { if (!obj) { - obj = helper; - helper = 0; + obj = helper + helper = 0 } if (typeof obj === 'function') { - obj = obj(this.test); + obj = obj(this.test) } if (!this.test.config) { - this.test.config = {}; + this.test.config = {} } - this.test.config[helper] = obj; - return this; + this.test.config[helper] = obj + return this } /** @@ -82,10 +82,10 @@ class ScenarioConfig { * @param {string} tagName */ tag(tagName) { - if (tagName[0] !== '@') tagName = `@${tagName}`; - if (!this.test.tags) this.test.tags = []; - this.test.tags.push(tagName); - return this; + if (tagName[0] !== '@') tagName = `@${tagName}` + if (!this.test.tags) this.test.tags = [] + this.test.tags.push(tagName) + return this } /** @@ -95,10 +95,10 @@ class ScenarioConfig { */ injectDependencies(dependencies) { Object.keys(dependencies).forEach(key => { - this.test.inject[key] = dependencies[key]; - }); - return this; + this.test.inject[key] = dependencies[key] + }) + return this } } -module.exports = ScenarioConfig; +module.exports = ScenarioConfig diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js index cde54cdf1..2c0eabad5 100644 --- a/lib/mocha/ui.js +++ b/lib/mocha/ui.js @@ -1,24 +1,24 @@ -const escapeRe = require('escape-string-regexp'); -const Suite = require('mocha/lib/suite'); -const Test = require('mocha/lib/test'); +const escapeRe = require('escape-string-regexp') +const Suite = require('mocha/lib/suite') +const Test = require('mocha/lib/test') -const scenario = require('./scenario'); -const ScenarioConfig = require('./scenarioConfig'); -const FeatureConfig = require('./featureConfig'); -const addDataContext = require('../data/context'); -const container = require('../container'); +const scenario = require('./scenario') +const ScenarioConfig = require('./scenarioConfig') +const FeatureConfig = require('./featureConfig') +const addDataContext = require('../data/context') +const container = require('../container') const setContextTranslation = context => { - const contexts = container.translation().value('contexts'); + const contexts = container.translation().value('contexts') if (contexts) { for (const key of Object.keys(contexts)) { if (context[key]) { - context[contexts[key]] = context[key]; + context[contexts[key]] = context[key] } } } -}; +} /** * Codecept-style interface: @@ -35,49 +35,49 @@ const setContextTranslation = context => { * @ignore */ module.exports = function (suite) { - const suites = [suite]; - suite.timeout(0); - let afterAllHooks; - let afterEachHooks; - let afterAllHooksAreLoaded; - let afterEachHooksAreLoaded; + const suites = [suite] + suite.timeout(0) + let afterAllHooks + let afterEachHooks + let afterAllHooksAreLoaded + let afterEachHooksAreLoaded suite.on('pre-require', (context, file, mocha) => { - const common = require('mocha/lib/interfaces/common')(suites, context, mocha); + const common = require('mocha/lib/interfaces/common')(suites, context, mocha) const addScenario = function (title, opts = {}, fn) { - const suite = suites[0]; + const suite = suites[0] if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; + fn = opts + opts = {} } if (suite.pending) { - fn = null; + fn = null } - const test = new Test(title, fn); - test.fullTitle = () => `${suite.title}: ${test.title}`; + const test = new Test(title, fn) + test.fullTitle = () => `${suite.title}: ${test.title}` - test.tags = (suite.tags || []).concat(title.match(/(\@[a-zA-Z0-9-_]+)/g) || []); // match tags from title - test.file = file; + test.tags = (suite.tags || []).concat(title.match(/(\@[a-zA-Z0-9-_]+)/g) || []) // match tags from title + test.file = file if (!test.inject) { - test.inject = {}; + test.inject = {} } - suite.addTest(scenario.test(test)); - if (opts.retries) test.retries(opts.retries); - if (opts.timeout) test.totalTimeout = opts.timeout; - test.opts = opts; + suite.addTest(scenario.test(test)) + if (opts.retries) test.retries(opts.retries) + if (opts.timeout) test.totalTimeout = opts.timeout + test.opts = opts - return new ScenarioConfig(test); - }; + return new ScenarioConfig(test) + } // create dispatcher - context.BeforeAll = common.before; - context.AfterAll = common.after; + context.BeforeAll = common.before + context.AfterAll = common.after - context.run = mocha.options.delay && common.runWithSuite(suite); + context.run = mocha.options.delay && common.runWithSuite(suite) /** * Describe a "suite" with the given `title` * and callback `fn` containing nested suites @@ -90,39 +90,39 @@ module.exports = function (suite) { context.Feature = function (title, opts) { if (suites.length > 1) { - suites.shift(); + suites.shift() } - afterAllHooks = []; - afterEachHooks = []; - afterAllHooksAreLoaded = false; - afterEachHooksAreLoaded = false; + afterAllHooks = [] + afterEachHooks = [] + afterAllHooksAreLoaded = false + afterEachHooksAreLoaded = false - const suite = Suite.create(suites[0], title); - if (!opts) opts = {}; - suite.opts = opts; - suite.timeout(0); + const suite = Suite.create(suites[0], title) + if (!opts) opts = {} + suite.opts = opts + suite.timeout(0) - if (opts.retries) suite.retries(opts.retries); - if (opts.timeout) suite.totalTimeout = opts.timeout; + if (opts.retries) suite.retries(opts.retries) + if (opts.timeout) suite.totalTimeout = opts.timeout - suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || []; // match tags from title - suite.file = file; - suite.fullTitle = () => `${suite.title}:`; - suites.unshift(suite); - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)); - afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]); + suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || [] // match tags from title + suite.file = file + suite.fullTitle = () => `${suite.title}:` + suites.unshift(suite) + suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) + afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]) - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)); - afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]); + suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) + afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]) if (opts.skipInfo && opts.skipInfo.skipped) { - suite.pending = true; - suite.opts = { ...suite.opts, skipInfo: opts.skipInfo }; + suite.pending = true + suite.opts = { ...suite.opts, skipInfo: opts.skipInfo } } - return new FeatureConfig(suite); - }; + return new FeatureConfig(suite) + } /** * Pending test suite. @@ -134,25 +134,25 @@ module.exports = function (suite) { const skipInfo = { skipped: true, message: 'Skipped due to "skip" on Feature.', - }; - return context.Feature(title, { ...opts, skipInfo }); - }; + } + return context.Feature(title, { ...opts, skipInfo }) + } context.BeforeSuite = function (fn) { - suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')); - }; + suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')) + } context.AfterSuite = function (fn) { - afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]); - }; + afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]) + } context.Background = context.Before = function (fn) { - suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')); - }; + suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')) + } context.After = function (fn) { - afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]); - }; + afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]) + } /** * Describe a specification or test-case @@ -160,17 +160,17 @@ module.exports = function (suite) { * acting as a thunk. * @ignore */ - context.Scenario = addScenario; + context.Scenario = addScenario /** * Exclusive test-case. * @ignore */ context.Scenario.only = function (title, opts, fn) { - const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`; - mocha.grep(new RegExp(reString)); - process.env.SCENARIO_ONLY = true; - return addScenario(title, opts, fn); - }; + const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}` + mocha.grep(new RegExp(reString)) + process.env.SCENARIO_ONLY = true + return addScenario(title, opts, fn) + } /** * Pending test case. @@ -180,11 +180,11 @@ module.exports = function (suite) { */ context.xScenario = context.Scenario.skip = function (title, opts = {}, fn) { if (typeof opts === 'function' && !fn) { - opts = {}; + opts = {} } - return context.Scenario(title, opts); - }; + return context.Scenario(title, opts) + } /** * Pending test case with message: 'Test not implemented!'. @@ -194,26 +194,26 @@ module.exports = function (suite) { */ context.Scenario.todo = function (title, opts = {}, fn) { if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; + fn = opts + opts = {} } const skipInfo = { message: 'Test not implemented!', description: fn ? fn.toString() : '', - }; + } - return context.Scenario(title, { ...opts, skipInfo }); - }; + return context.Scenario(title, { ...opts, skipInfo }) + } /** * For translation */ - setContextTranslation(context); + setContextTranslation(context) - addDataContext(context); - }); + addDataContext(context) + }) suite.on('post-require', () => { /** @@ -221,16 +221,16 @@ module.exports = function (suite) { */ if (!afterEachHooksAreLoaded && Array.isArray(afterEachHooks)) { afterEachHooks.forEach(hook => { - suites[0].afterEach(hook[0], hook[1]); - }); - afterEachHooksAreLoaded = true; + suites[0].afterEach(hook[0], hook[1]) + }) + afterEachHooksAreLoaded = true } if (!afterAllHooksAreLoaded && Array.isArray(afterAllHooks)) { afterAllHooks.forEach(hook => { - suites[0].afterAll(hook[0], hook[1]); - }); - afterAllHooksAreLoaded = true; + suites[0].afterAll(hook[0], hook[1]) + }) + afterAllHooksAreLoaded = true } - }); -}; + }) +} From bfa9bdda943507df6c40a871606daa64ec1d9136 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sat, 4 Jan 2025 04:29:18 +0200 Subject: [PATCH 05/13] updated tests --- test/unit/assert/empty_test.js | 46 +-- test/unit/assert/equal_test.js | 50 +-- test/unit/assert/include_test.js | 50 +-- test/unit/bdd_test.js | 452 ++++++++++----------- test/unit/data/dataTableArgument_test.js | 76 ++-- test/unit/data/table_test.js | 134 +++--- test/unit/data/ui_test.js | 130 +++--- test/unit/helper/FileSystem_test.js | 72 ++-- test/unit/helper/element_not_found_test.js | 38 +- test/unit/plugin/customLocator_test.js | 92 ++--- test/unit/plugin/eachElement_test.js | 58 +-- test/unit/plugin/retryFailedStep_test.js | 345 +++++++++------- test/unit/plugin/retryto_test.js | 56 +-- test/unit/plugin/screenshotOnFail_test.js | 112 ++--- test/unit/plugin/subtitles_test.js | 207 +++++----- test/unit/plugin/tryTo_test.js | 38 +- test/unit/scenario_test.js | 128 +++--- test/unit/ui_test.js | 282 ++++++------- 18 files changed, 1231 insertions(+), 1135 deletions(-) diff --git a/test/unit/assert/empty_test.js b/test/unit/assert/empty_test.js index 299d37392..19d989de0 100644 --- a/test/unit/assert/empty_test.js +++ b/test/unit/assert/empty_test.js @@ -1,37 +1,37 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const { Assertion } = require('../../../lib/assert/empty'); -const AssertionError = require('../../../lib/assert/error'); +const { Assertion } = require('../../../lib/assert/empty') +const AssertionError = require('../../../lib/assert/error') -let empty; +let empty describe('empty assertion', () => { beforeEach(() => { - empty = new Assertion({ subject: 'web page' }); - }); + empty = new Assertion({ subject: 'web page' }) + }) it('should check for something to be empty', () => { - empty.assert(null); - expect(() => empty.negate(null)).to.throw(AssertionError); - }); + empty.assert(null) + expect(() => empty.negate(null)).to.throw(AssertionError) + }) it('should check for something not to be empty', () => { - empty.negate('something'); - expect(() => empty.assert('something')).to.throw(AssertionError); - }); + empty.negate('something') + expect(() => empty.assert('something')).to.throw(AssertionError) + }) it('should provide nice assert error message', () => { - empty.params.value = '/nothing'; - const err = empty.getFailedAssertion(); - expect(err.inspect()).to.equal("expected web page '/nothing' to be empty"); - }); + empty.params.value = '/nothing' + const err = empty.getFailedAssertion() + expect(err.inspect()).to.equal("expected web page '/nothing' to be empty") + }) it('should provide nice negate error message', () => { - empty.params.value = '/nothing'; - const err = empty.getFailedNegation(); - expect(err.inspect()).to.equal("expected web page '/nothing' not to be empty"); - }); -}); + empty.params.value = '/nothing' + const err = empty.getFailedNegation() + expect(err.inspect()).to.equal("expected web page '/nothing' not to be empty") + }) +}) diff --git a/test/unit/assert/equal_test.js b/test/unit/assert/equal_test.js index 232a715b2..bfe513721 100644 --- a/test/unit/assert/equal_test.js +++ b/test/unit/assert/equal_test.js @@ -1,39 +1,39 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const { Assertion } = require('../../../lib/assert/equal'); -const AssertionError = require('../../../lib/assert/error'); +const { Assertion } = require('../../../lib/assert/equal') +const AssertionError = require('../../../lib/assert/error') -let equal; +let equal describe('equal assertion', () => { beforeEach(() => { - equal = new Assertion({ jar: 'contents of webpage' }); - }); + equal = new Assertion({ jar: 'contents of webpage' }) + }) it('should check for equality', () => { - equal.assert('hello', 'hello'); - expect(() => equal.negate('hello', 'hello')).to.throw(AssertionError); - }); + equal.assert('hello', 'hello') + expect(() => equal.negate('hello', 'hello')).to.throw(AssertionError) + }) it('should check for something not to be equal', () => { - equal.negate('hello', 'hi'); - expect(() => equal.assert('hello', 'hi')).to.throw(AssertionError); - }); + equal.negate('hello', 'hi') + expect(() => equal.assert('hello', 'hi')).to.throw(AssertionError) + }) it('should provide nice assert error message', () => { - equal.params.expected = 'hello'; - equal.params.actual = 'hi'; - const err = equal.getFailedAssertion(); - expect(err.inspect()).to.equal('expected contents of webpage "hello" to equal "hi"'); - }); + equal.params.expected = 'hello' + equal.params.actual = 'hi' + const err = equal.getFailedAssertion() + expect(err.inspect()).to.equal('expected contents of webpage "hello" to equal "hi"') + }) it('should provide nice negate error message', () => { - equal.params.expected = 'hello'; - equal.params.actual = 'hello'; - const err = equal.getFailedNegation(); - expect(err.inspect()).to.equal('expected contents of webpage "hello" not to equal "hello"'); - }); -}); + equal.params.expected = 'hello' + equal.params.actual = 'hello' + const err = equal.getFailedNegation() + expect(err.inspect()).to.equal('expected contents of webpage "hello" not to equal "hello"') + }) +}) diff --git a/test/unit/assert/include_test.js b/test/unit/assert/include_test.js index 7bbe78661..6a60372f2 100644 --- a/test/unit/assert/include_test.js +++ b/test/unit/assert/include_test.js @@ -1,39 +1,39 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const Assertion = require('../../../lib/assert/include').Assertion; -const AssertionError = require('../../../lib/assert/error'); +const Assertion = require('../../../lib/assert/include').Assertion +const AssertionError = require('../../../lib/assert/error') -let equal; +let equal describe('equal assertion', () => { beforeEach(() => { - equal = new Assertion({ jar: 'contents of webpage' }); - }); + equal = new Assertion({ jar: 'contents of webpage' }) + }) it('should check for inclusion', () => { - equal.assert('h', 'hello'); - expect(() => equal.negate('h', 'hello')).to.throw(AssertionError); - }); + equal.assert('h', 'hello') + expect(() => equal.negate('h', 'hello')).to.throw(AssertionError) + }) it('should check !include', () => { - equal.negate('x', 'hello'); - expect(() => equal.assert('x', 'hello')).to.throw(AssertionError); - }); + equal.negate('x', 'hello') + expect(() => equal.assert('x', 'hello')).to.throw(AssertionError) + }) it('should provide nice assert error message', () => { - equal.params.needle = 'hello'; - equal.params.haystack = 'x'; - const err = equal.getFailedAssertion(); - expect(err.inspect()).to.equal('expected contents of webpage to include "hello"'); - }); + equal.params.needle = 'hello' + equal.params.haystack = 'x' + const err = equal.getFailedAssertion() + expect(err.inspect()).to.equal('expected contents of webpage to include "hello"') + }) it('should provide nice negate error message', () => { - equal.params.needle = 'hello'; - equal.params.haystack = 'h'; - const err = equal.getFailedNegation(); - expect(err.inspect()).to.equal('expected contents of webpage not to include "hello"'); - }); -}); + equal.params.needle = 'hello' + equal.params.haystack = 'h' + const err = equal.getFailedNegation() + expect(err.inspect()).to.equal('expected contents of webpage not to include "hello"') + }) +}) diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index a2249c633..f41c4fc79 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -1,25 +1,25 @@ -const Gherkin = require('@cucumber/gherkin'); -const Messages = require('@cucumber/messages'); +const Gherkin = require('@cucumber/gherkin') +const Messages = require('@cucumber/messages') -const chai = require('chai'); +const chai = require('chai') -const expect = chai.expect; +const expect = chai.expect -const uuidFn = Messages.IdGenerator.uuid(); -const builder = new Gherkin.AstBuilder(uuidFn); -const matcher = new Gherkin.GherkinClassicTokenMatcher(); +const uuidFn = Messages.IdGenerator.uuid() +const builder = new Gherkin.AstBuilder(uuidFn) +const matcher = new Gherkin.GherkinClassicTokenMatcher() -const Config = require('../../lib/config'); -const { Given, When, And, Then, matchStep, clearSteps, defineParameterType } = require('../../lib/mocha/bdd'); -const run = require('../../lib/mocha/gherkin'); -const recorder = require('../../lib/recorder'); -const container = require('../../lib/container'); -const actor = require('../../lib/actor'); -const event = require('../../lib/event'); +const Config = require('../../lib/config') +const { Given, When, And, Then, matchStep, clearSteps, defineParameterType } = require('../../lib/mocha/bdd') +const run = require('../../lib/mocha/gherkin') +const recorder = require('../../lib/recorder') +const container = require('../../lib/container') +const actor = require('../../lib/actor') +const event = require('../../lib/event') class Color { constructor(name) { - this.name = name; + this.name = name } } @@ -34,186 +34,186 @@ const text = ` Given I have product with 600 price And I have product with 1000 price When I go to checkout process -`; +` const checkTestForErrors = test => { return new Promise((resolve, reject) => { test.fn(err => { if (err) { - return reject(err); + return reject(err) } - resolve(); - }); - }); -}; + resolve() + }) + }) +} describe('BDD', () => { beforeEach(() => { - clearSteps(); - recorder.start(); - container.create({}); - Config.reset(); - }); + clearSteps() + recorder.start() + container.create({}) + Config.reset() + }) afterEach(() => { - container.clear(); - recorder.stop(); - }); + container.clear() + recorder.stop() + }) it('should parse gherkin input', () => { - const parser = new Gherkin.Parser(builder, matcher); - parser.stopAtFirstError = false; - const ast = parser.parse(text); + const parser = new Gherkin.Parser(builder, matcher) + parser.stopAtFirstError = false + const ast = parser.parse(text) // console.log('Feature', ast.feature); // console.log('Scenario', ast.feature.children); // console.log('Steps', ast.feature.children[0].steps[0]); - expect(ast.feature).is.ok; - expect(ast.feature.children).is.ok; - expect(ast.feature.children[0].scenario.steps).is.ok; - }); + expect(ast.feature).is.ok + expect(ast.feature.children).is.ok + expect(ast.feature.children[0].scenario.steps).is.ok + }) it('should load step definitions', () => { - Given('I am a bird', () => 1); - When('I fly over ocean', () => 2); - And(/^I fly over land$/i, () => 3); - Then(/I see (.*?)/, () => 4); - expect(1).is.equal(matchStep('I am a bird')()); - expect(3).is.equal(matchStep('I Fly oVer Land')()); - expect(4).is.equal(matchStep('I see ocean')()); - expect(4).is.equal(matchStep('I see world')()); - }); + Given('I am a bird', () => 1) + When('I fly over ocean', () => 2) + And(/^I fly over land$/i, () => 3) + Then(/I see (.*?)/, () => 4) + expect(1).is.equal(matchStep('I am a bird')()) + expect(3).is.equal(matchStep('I Fly oVer Land')()) + expect(4).is.equal(matchStep('I see ocean')()) + expect(4).is.equal(matchStep('I see world')()) + }) it('should fail on duplicate step definitions with option', () => { Config.append({ gherkin: { avoidDuplicateSteps: true, }, - }); + }) - let error = null; + let error = null try { - Given('I am a bird', () => 1); - Then('I am a bird', () => 1); + Given('I am a bird', () => 1) + Then('I am a bird', () => 1) } catch (err) { - error = err; + error = err } finally { - expect(!!error).is.true; + expect(!!error).is.true } - }); + }) it('should contain tags', async () => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); - When('I go to checkout process', () => (sum += 10)); - const suite = await run(text); - suite.tests[0].fn(() => {}); - expect(suite.tests[0].tags).is.ok; - expect('@super').is.equal(suite.tests[0].tags[0]); - }); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) + When('I go to checkout process', () => (sum += 10)) + const suite = await run(text) + suite.tests[0].fn(() => {}) + expect(suite.tests[0].tags).is.ok + expect('@super').is.equal(suite.tests[0].tags[0]) + }) it('should load step definitions', done => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); - When('I go to checkout process', () => (sum += 10)); - const suite = run(text); - expect('checkout process').is.equal(suite.title); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) + When('I go to checkout process', () => (sum += 10)) + const suite = run(text) + expect('checkout process').is.equal(suite.title) suite.tests[0].fn(() => { - expect(suite.tests[0].steps).is.ok; - expect(1610).is.equal(sum); - done(); - }); - }); + expect(suite.tests[0].steps).is.ok + expect(1610).is.equal(sum) + done() + }) + }) it('should allow failed steps', async () => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); - When('I go to checkout process', () => expect(false).is.true); - const suite = run(text); - expect('checkout process').is.equal(suite.title); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) + When('I go to checkout process', () => expect(false).is.true) + const suite = run(text) + expect('checkout process').is.equal(suite.title) try { - await checkTestForErrors(suite.tests[0]); - return Promise.reject(new Error('Test should have thrown with failed step, but did not')); + await checkTestForErrors(suite.tests[0]) + return Promise.reject(new Error('Test should have thrown with failed step, but did not')) } catch (err) { - const errored = !!err; - expect(errored).is.true; + const errored = !!err + expect(errored).is.true } - }); + }) it('handles errors in steps', async () => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) When('I go to checkout process', () => { - throw new Error('errored step'); - }); - const suite = run(text); - expect('checkout process').is.equal(suite.title); + throw new Error('errored step') + }) + const suite = run(text) + expect('checkout process').is.equal(suite.title) try { - await checkTestForErrors(suite.tests[0]); - return Promise.reject(new Error('Test should have thrown with error, but did not')); + await checkTestForErrors(suite.tests[0]) + return Promise.reject(new Error('Test should have thrown with error, but did not')) } catch (err) { - const errored = !!err; - expect(errored).is.true; + const errored = !!err + expect(errored).is.true } - }); + }) it('handles async errors in steps', async () => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); - When('I go to checkout process', () => Promise.reject(new Error('step failed'))); - const suite = run(text); - expect('checkout process').is.equal(suite.title); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) + When('I go to checkout process', () => Promise.reject(new Error('step failed'))) + const suite = run(text) + expect('checkout process').is.equal(suite.title) try { - await checkTestForErrors(suite.tests[0]); - return Promise.reject(new Error('Test should have thrown with error, but did not')); + await checkTestForErrors(suite.tests[0]) + return Promise.reject(new Error('Test should have thrown with error, but did not')) } catch (err) { - const errored = !!err; - expect(errored).is.true; + const errored = !!err + expect(errored).is.true } - }); + }) it('should work with async functions', done => { - let sum = 0; - Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))); + let sum = 0 + Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) When('I go to checkout process', async () => { return new Promise(checkoutDone => { - sum += 10; - setTimeout(checkoutDone, 0); - }); - }); - const suite = run(text); - expect('checkout process').is.equal(suite.title); + sum += 10 + setTimeout(checkoutDone, 0) + }) + }) + const suite = run(text) + expect('checkout process').is.equal(suite.title) suite.tests[0].fn(() => { - expect(suite.tests[0].steps).is.ok; - expect(1610).is.equal(sum); - done(); - }); - }); + expect(suite.tests[0].steps).is.ok + expect(1610).is.equal(sum) + done() + }) + }) it('should execute scenarios step-by-step ', async () => { - recorder.start(); - printed = []; + recorder.start() + printed = [] container.append({ helpers: { simple: { do(...args) { - return Promise.resolve().then(() => printed.push(args.join(' '))); + return Promise.resolve().then(() => printed.push(args.join(' '))) }, }, }, - }); - I = actor(); - let sum = 0; + }) + I = actor() + let sum = 0 Given(/I have product with (\d+) price/, price => { - I.do('add', (sum += parseInt(price, 10))); - }); + I.do('add', (sum += parseInt(price, 10))) + }) When('I go to checkout process', () => { - I.do('add finish checkout'); - }); - const suite = run(text); + I.do('add finish checkout') + }) + const suite = run(text) suite.tests[0].fn(() => { recorder.promise().then(() => { - printed.should.include.members(['add 600', 'add 1600', 'add finish checkout']); - const lines = recorder.scheduled().split('\n'); + printed.should.include.members(['add 600', 'add 1600', 'add finish checkout']) + const lines = recorder.scheduled().split('\n') lines.should.include.members([ 'do: "add", 600', 'step passed', @@ -226,17 +226,17 @@ describe('BDD', () => { 'return result', 'fire test.passed', 'finish test', - ]); - done(); - }); - }); - }); + ]) + done() + }) + }) + }) it('should match step with params', () => { - Given('I am a {word}', param => param); - const fn = matchStep('I am a bird'); - expect('bird').is.equal(fn.params[0]); - }); + Given('I am a {word}', param => param) + const fn = matchStep('I am a bird') + expect('bird').is.equal(fn.params[0]) + }) it('should produce step events', done => { const text = ` @@ -244,34 +244,34 @@ describe('BDD', () => { Scenario: Then I emit step events - `; - Then('I emit step events', () => {}); - let listeners = 0; - event.dispatcher.addListener(event.bddStep.before, () => listeners++); - event.dispatcher.addListener(event.bddStep.after, () => listeners++); + ` + Then('I emit step events', () => {}) + let listeners = 0 + event.dispatcher.addListener(event.bddStep.before, () => listeners++) + event.dispatcher.addListener(event.bddStep.after, () => listeners++) - const suite = run(text); + const suite = run(text) suite.tests[0].fn(() => { - listeners.should.eql(2); - done(); - }); - }); + listeners.should.eql(2) + done() + }) + }) it('should use shortened form for step definitions', () => { - let fn; - Given('I am a {word}', params => params[0]); - When('I have {int} wings and {int} eyes', params => params[0] + params[1]); - Given('I have ${int} in my pocket', params => params[0]); // eslint-disable-line no-template-curly-in-string - Given('I have also ${float} in my pocket', params => params[0]); // eslint-disable-line no-template-curly-in-string - fn = matchStep('I am a bird'); - expect('bird').is.equal(fn(fn.params)); - fn = matchStep('I have 2 wings and 2 eyes'); - expect(4).is.equal(fn(fn.params)); - fn = matchStep('I have $500 in my pocket'); - expect(500).is.equal(fn(fn.params)); - fn = matchStep('I have also $500.30 in my pocket'); - expect(500.3).is.equal(fn(fn.params)); - }); + let fn + Given('I am a {word}', params => params[0]) + When('I have {int} wings and {int} eyes', params => params[0] + params[1]) + Given('I have ${int} in my pocket', params => params[0]) // eslint-disable-line no-template-curly-in-string + Given('I have also ${float} in my pocket', params => params[0]) // eslint-disable-line no-template-curly-in-string + fn = matchStep('I am a bird') + expect('bird').is.equal(fn(fn.params)) + fn = matchStep('I have 2 wings and 2 eyes') + expect(4).is.equal(fn(fn.params)) + fn = matchStep('I have $500 in my pocket') + expect(500).is.equal(fn(fn.params)) + fn = matchStep('I have also $500.30 in my pocket') + expect(500.3).is.equal(fn(fn.params)) + }) it('should attach before hook for Background', finish => { const text = ` @@ -282,22 +282,22 @@ describe('BDD', () => { Scenario: Then I am shopping - `; - let sum = 0; + ` + let sum = 0 function incrementSum() { - sum++; + sum++ } - Given('I am logged in as customer', incrementSum); - Then('I am shopping', incrementSum); - const suite = run(text); - const done = () => {}; + Given('I am logged in as customer', incrementSum) + Then('I am shopping', incrementSum) + const suite = run(text) + const done = () => {} - suite._beforeEach.forEach(hook => hook.run(done)); + suite._beforeEach.forEach(hook => hook.run(done)) suite.tests[0].fn(() => { - expect(sum).is.equal(2); - finish(); - }); - }); + expect(sum).is.equal(2) + finish() + }) + }) it('should execute scenario outlines', done => { const text = ` @@ -319,37 +319,37 @@ describe('BDD', () => { Examples: | price | total | | 20 | 18 | - `; - let cart = 0; - let sum = 0; + ` + let cart = 0 + let sum = 0 Given('I have product with price {int}$ in my cart', price => { - cart = price; - }); + cart = price + }) Given('discount is {int} %', discount => { - cart -= (cart * discount) / 100; - }); + cart -= (cart * discount) / 100 + }) Then('I should see price is {string} $', total => { - sum = parseInt(total, 10); - }); + sum = parseInt(total, 10) + }) - const suite = run(text); + const suite = run(text) - expect(suite.tests[0].tags).is.ok; - expect(['@awesome', '@cool', '@super']).is.deep.equal(suite.tests[0].tags); - expect(['@awesome', '@cool', '@super', '@exampleTag1', '@exampleTag2']).is.deep.equal(suite.tests[1].tags); + expect(suite.tests[0].tags).is.ok + expect(['@awesome', '@cool', '@super']).is.deep.equal(suite.tests[0].tags) + expect(['@awesome', '@cool', '@super', '@exampleTag1', '@exampleTag2']).is.deep.equal(suite.tests[1].tags) - expect(2).is.equal(suite.tests.length); + expect(2).is.equal(suite.tests.length) suite.tests[0].fn(() => { - expect(9).is.equal(cart); - expect(9).is.equal(sum); + expect(9).is.equal(cart) + expect(9).is.equal(sum) suite.tests[1].fn(() => { - expect(18).is.equal(cart); - expect(18).is.equal(sum); - done(); - }); - }); - }); + expect(18).is.equal(cart) + expect(18).is.equal(sum) + done() + }) + }) + }) it('should provide a parsed DataTable', done => { const text = ` @@ -366,59 +366,59 @@ describe('BDD', () => { | label | price | | beer | 9 | | cookies | 12 | - `; + ` - let givenParsedRows; - let thenParsedRows; + let givenParsedRows + let thenParsedRows Given('I have the following products :', products => { - expect(products.rows.length).to.equal(3); - givenParsedRows = products.parse(); - }); + expect(products.rows.length).to.equal(3) + givenParsedRows = products.parse() + }) Then('I should see the following products :', products => { - expect(products.rows.length).to.equal(3); - thenParsedRows = products.parse(); - }); + expect(products.rows.length).to.equal(3) + thenParsedRows = products.parse() + }) - const suite = run(text); + const suite = run(text) const expectedParsedDataTable = [ ['label', 'price'], ['beer', '9'], ['cookies', '12'], - ]; + ] suite.tests[0].fn(() => { - expect(givenParsedRows.rawData).is.deep.equal(expectedParsedDataTable); - expect(thenParsedRows.rawData).is.deep.equal(expectedParsedDataTable); - done(); - }); - }); + expect(givenParsedRows.rawData).is.deep.equal(expectedParsedDataTable) + expect(thenParsedRows.rawData).is.deep.equal(expectedParsedDataTable) + done() + }) + }) it('should match step with custom parameter type', done => { const colorType = { name: 'color', regexp: /red|blue|yellow/, transformer: s => new Color(s), - }; - defineParameterType(colorType); - Given('I have a {color} label', color => color); - const fn = matchStep('I have a red label'); - expect('red').is.equal(fn.params[0].name); - done(); - }); + } + defineParameterType(colorType) + Given('I have a {color} label', color => color) + const fn = matchStep('I have a red label') + expect('red').is.equal(fn.params[0].name) + done() + }) it('should match step with async custom parameter type transformation', async () => { const colorType = { name: 'async_color', regexp: /red|blue|yellow/, transformer: async s => new Color(s), - }; - defineParameterType(colorType); - Given('I have a {async_color} label', color => color); - const fn = matchStep('I have a blue label'); - const color = await fn.params[0]; - expect('blue').is.equal(color.name); - await Promise.resolve(); - }); -}); + } + defineParameterType(colorType) + Given('I have a {async_color} label', color => color) + const fn = matchStep('I have a blue label') + const color = await fn.params[0] + expect('blue').is.equal(color.name) + await Promise.resolve() + }) +}) diff --git a/test/unit/data/dataTableArgument_test.js b/test/unit/data/dataTableArgument_test.js index b62c187bd..999186ade 100644 --- a/test/unit/data/dataTableArgument_test.js +++ b/test/unit/data/dataTableArgument_test.js @@ -1,9 +1,9 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const { it } = require('mocha'); -const DataTableArgument = require('../../../lib/data/dataTableArgument'); + expect = chai.expect +}) +const { it } = require('mocha') +const DataTableArgument = require('../../../lib/data/dataTableArgument') describe('DataTableArgument', () => { const gherkinDataTable = { @@ -25,7 +25,7 @@ describe('DataTableArgument', () => { ], }, ], - }; + } const gherkinDataTableWithHeader = { rows: [ @@ -46,7 +46,7 @@ describe('DataTableArgument', () => { ], }, ], - }; + } const gherkinDataTableWithColumnHeader = { rows: [ @@ -67,41 +67,47 @@ describe('DataTableArgument', () => { ], }, ], - }; + } it('should return a 2D array containing each row', () => { - const dta = new DataTableArgument(gherkinDataTable); - const raw = dta.raw(); - const expectedRaw = [['John', 'Doe'], ['Chuck', 'Norris']]; - expect(raw).to.deep.equal(expectedRaw); - }); + const dta = new DataTableArgument(gherkinDataTable) + const raw = dta.raw() + const expectedRaw = [ + ['John', 'Doe'], + ['Chuck', 'Norris'], + ] + expect(raw).to.deep.equal(expectedRaw) + }) it('should return a 2D array containing each row without the header (first one)', () => { - const dta = new DataTableArgument(gherkinDataTableWithHeader); - const rows = dta.rows(); - const expectedRows = [['Chuck', 'Norris']]; - expect(rows).to.deep.equal(expectedRows); - }); + const dta = new DataTableArgument(gherkinDataTableWithHeader) + const rows = dta.rows() + const expectedRows = [['Chuck', 'Norris']] + expect(rows).to.deep.equal(expectedRows) + }) it('should return an of object where properties is the header', () => { - const dta = new DataTableArgument(gherkinDataTableWithHeader); - const rows = dta.hashes(); - const expectedRows = [{ firstName: 'Chuck', lastName: 'Norris' }]; - expect(rows).to.deep.equal(expectedRows); - }); + const dta = new DataTableArgument(gherkinDataTableWithHeader) + const rows = dta.hashes() + const expectedRows = [{ firstName: 'Chuck', lastName: 'Norris' }] + expect(rows).to.deep.equal(expectedRows) + }) it('transpose should transpose the gherkin data table', () => { - const dta = new DataTableArgument(gherkinDataTable); - dta.transpose(); - const raw = dta.raw(); - const expectedRaw = [['John', 'Chuck'], ['Doe', 'Norris']]; - expect(raw).to.deep.equal(expectedRaw); - }); + const dta = new DataTableArgument(gherkinDataTable) + dta.transpose() + const raw = dta.raw() + const expectedRaw = [ + ['John', 'Chuck'], + ['Doe', 'Norris'], + ] + expect(raw).to.deep.equal(expectedRaw) + }) it('rowsHash returns an object where the keys are the first column', () => { - const dta = new DataTableArgument(gherkinDataTableWithColumnHeader); - const rawHash = dta.rowsHash(); - const expectedRaw = { firstName: 'Chuck', lastName: 'Norris' }; - expect(rawHash).to.deep.equal(expectedRaw); - }); -}); + const dta = new DataTableArgument(gherkinDataTableWithColumnHeader) + const rawHash = dta.rowsHash() + const expectedRaw = { firstName: 'Chuck', lastName: 'Norris' } + expect(rawHash).to.deep.equal(expectedRaw) + }) +}) diff --git a/test/unit/data/table_test.js b/test/unit/data/table_test.js index ae209f56b..23aa15903 100644 --- a/test/unit/data/table_test.js +++ b/test/unit/data/table_test.js @@ -1,91 +1,97 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const DataTable = require('../../../lib/data/table'); +const DataTable = require('../../../lib/data/table') describe('DataTable', () => { it('should take an array for creation', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - expect(dataTable.array).to.deep.equal(data); - expect(dataTable.rows).to.deep.equal([]); - }); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + expect(dataTable.array).to.deep.equal(data) + expect(dataTable.rows).to.deep.equal([]) + }) it('should allow arrays to be added', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - dataTable.add(['jon', 'snow']); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + dataTable.add(['jon', 'snow']) const expected = { login: 'jon', password: 'snow', - }; - expect(JSON.stringify(dataTable.rows[0].data)).to.equal(JSON.stringify(expected)); - }); + } + expect(JSON.stringify(dataTable.rows[0].data)).to.equal(JSON.stringify(expected)) + }) it('should not allow an empty array to be added', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - expect(() => dataTable.add([])).to.throw(); - }); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + expect(() => dataTable.add([])).to.throw() + }) it('should not allow an array with more slots than the original to be added', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - expect(() => dataTable.add(['Henrietta'])).to.throw(); - }); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + expect(() => dataTable.add(['Henrietta'])).to.throw() + }) it('should not allow an array with less slots than the original to be added', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - expect(() => dataTable.add(['Acid', 'Jazz', 'Singer'])).to.throw(); - }); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + expect(() => dataTable.add(['Acid', 'Jazz', 'Singer'])).to.throw() + }) it('should filter an array', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - dataTable.add(['jon', 'snow']); - dataTable.add(['tyrion', 'lannister']); - dataTable.add(['jaime', 'lannister']); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + dataTable.add(['jon', 'snow']) + dataTable.add(['tyrion', 'lannister']) + dataTable.add(['jaime', 'lannister']) - const expected = [{ - skip: false, - data: { - login: 'tyrion', - password: 'lannister', + const expected = [ + { + skip: false, + data: { + login: 'tyrion', + password: 'lannister', + }, }, - }, { - skip: false, - data: { - login: 'jaime', - password: 'lannister', + { + skip: false, + data: { + login: 'jaime', + password: 'lannister', + }, }, - }]; - expect(JSON.stringify(dataTable.filter(row => row.password === 'lannister'))).to.equal(JSON.stringify(expected)); - }); + ] + expect(JSON.stringify(dataTable.filter(row => row.password === 'lannister'))).to.equal(JSON.stringify(expected)) + }) it('should filter an array with skips', () => { - const data = ['login', 'password']; - const dataTable = new DataTable(data); - dataTable.add(['jon', 'snow']); - dataTable.xadd(['tyrion', 'lannister']); - dataTable.add(['jaime', 'lannister']); + const data = ['login', 'password'] + const dataTable = new DataTable(data) + dataTable.add(['jon', 'snow']) + dataTable.xadd(['tyrion', 'lannister']) + dataTable.add(['jaime', 'lannister']) - const expected = [{ - skip: true, - data: { - login: 'tyrion', - password: 'lannister', + const expected = [ + { + skip: true, + data: { + login: 'tyrion', + password: 'lannister', + }, }, - }, { - skip: false, - data: { - login: 'jaime', - password: 'lannister', + { + skip: false, + data: { + login: 'jaime', + password: 'lannister', + }, }, - }]; - expect(JSON.stringify(dataTable.filter(row => row.password === 'lannister'))).to.equal(JSON.stringify(expected)); - }); -}); + ] + expect(JSON.stringify(dataTable.filter(row => row.password === 'lannister'))).to.equal(JSON.stringify(expected)) + }) +}) diff --git a/test/unit/data/ui_test.js b/test/unit/data/ui_test.js index 0190e74a2..6292d08c4 100644 --- a/test/unit/data/ui_test.js +++ b/test/unit/data/ui_test.js @@ -1,105 +1,105 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const Mocha = require('mocha/lib/mocha'); -const Suite = require('mocha/lib/suite'); + expect = chai.expect +}) +const Mocha = require('mocha/lib/mocha') +const Suite = require('mocha/lib/suite') -const makeUI = require('../../../lib/mocha/ui'); -const addData = require('../../../lib/data/context'); -const DataTable = require('../../../lib/data/table'); -const Secret = require('../../../lib/secret'); +const makeUI = require('../../../lib/mocha/ui') +const addData = require('../../../lib/data/context') +const DataTable = require('../../../lib/data/table') +const Secret = require('../../../lib/secret') describe('ui', () => { - let suite; - let context; - let dataTable; + let suite + let context + let dataTable beforeEach(() => { - context = {}; - suite = new Suite('empty', null); - makeUI(suite); - suite.emit('pre-require', context, {}, new Mocha()); - addData(context); - - dataTable = new DataTable(['username', 'password']); - dataTable.add(['jon', 'snow']); - dataTable.xadd(['tyrion', 'lannister']); - dataTable.add(['jaime', 'lannister']); - dataTable.add(['Username', new Secret('theSecretPassword')]); - }); + context = {} + suite = new Suite('empty', null) + makeUI(suite) + suite.emit('pre-require', context, {}, new Mocha()) + addData(context) + + dataTable = new DataTable(['username', 'password']) + dataTable.add(['jon', 'snow']) + dataTable.xadd(['tyrion', 'lannister']) + dataTable.add(['jaime', 'lannister']) + dataTable.add(['Username', new Secret('theSecretPassword')]) + }) describe('Data', () => { it('can add a tag to all scenarios', () => { - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.tag('@user'); + dataScenarioConfig.tag('@user') dataScenarioConfig.scenarios.forEach(scenario => { - expect(scenario.test.tags).to.include('@user'); - }); - }); + expect(scenario.test.tags).to.include('@user') + }) + }) it('can add a timeout to all scenarios', () => { - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.timeout(3); + dataScenarioConfig.timeout(3) - dataScenarioConfig.scenarios.forEach(scenario => expect(3).to.equal(scenario.test._timeout)); - }); + dataScenarioConfig.scenarios.forEach(scenario => expect(3).to.equal(scenario.test._timeout)) + }) it('can add retries to all scenarios', () => { - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.retry(3); + dataScenarioConfig.retry(3) - dataScenarioConfig.scenarios.forEach(scenario => expect(3).to.equal(scenario.test._retries)); - }); + dataScenarioConfig.scenarios.forEach(scenario => expect(3).to.equal(scenario.test._retries)) + }) it('can expect failure for all scenarios', () => { - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.fails(); + dataScenarioConfig.fails() - dataScenarioConfig.scenarios.forEach(scenario => expect(scenario.test.throws).to.exist); - }); + dataScenarioConfig.scenarios.forEach(scenario => expect(scenario.test.throws).to.exist) + }) it('can expect a specific error for all scenarios', () => { - const err = new Error(); + const err = new Error() - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.throws(err); + dataScenarioConfig.throws(err) - dataScenarioConfig.scenarios.forEach(scenario => expect(err).to.equal(scenario.test.throws)); - }); + dataScenarioConfig.scenarios.forEach(scenario => expect(err).to.equal(scenario.test.throws)) + }) it('can configure a helper for all scenarios', () => { - const helperName = 'myHelper'; - const helper = {}; + const helperName = 'myHelper' + const helper = {} - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) - dataScenarioConfig.config(helperName, helper); + dataScenarioConfig.config(helperName, helper) - dataScenarioConfig.scenarios.forEach(scenario => expect(helper).to.equal(scenario.test.config[helperName])); - }); + dataScenarioConfig.scenarios.forEach(scenario => expect(helper).to.equal(scenario.test.config[helperName])) + }) it("should shows object's toString() method in each scenario's name if the toString() method is overridden", () => { - const data = [{ toString: () => 'test case title' }]; - const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {}); - expect('scenario | test case title').to.equal(dataScenarioConfig.scenarios[0].test.title); - }); + const data = [{ toString: () => 'test case title' }] + const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {}) + expect('scenario | test case title').to.equal(dataScenarioConfig.scenarios[0].test.title) + }) it("should shows JSON.stringify() in each scenario's name if the toString() method isn't overridden", () => { - const data = [{ name: 'John Do' }]; - const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {}); - expect(`scenario | ${JSON.stringify(data[0])}`).to.equal(dataScenarioConfig.scenarios[0].test.title); - }); + const data = [{ name: 'John Do' }] + const dataScenarioConfig = context.Data(data).Scenario('scenario', () => {}) + expect(`scenario | ${JSON.stringify(data[0])}`).to.equal(dataScenarioConfig.scenarios[0].test.title) + }) it('should shows secret value as *****', () => { - const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}); - expect('scenario | {"username":"Username","password":"*****"}').to.equal(dataScenarioConfig.scenarios[2].test.title); - }); - }); -}); + const dataScenarioConfig = context.Data(dataTable).Scenario('scenario', () => {}) + expect('scenario | {"username":"Username","password":"*****"}').to.equal(dataScenarioConfig.scenarios[2].test.title) + }) + }) +}) diff --git a/test/unit/helper/FileSystem_test.js b/test/unit/helper/FileSystem_test.js index 81d22b8f3..ef1ac4ebd 100644 --- a/test/unit/helper/FileSystem_test.js +++ b/test/unit/helper/FileSystem_test.js @@ -1,58 +1,58 @@ -const path = require('path'); +const path = require('path') -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const FileSystem = require('../../../lib/helper/FileSystem'); +const FileSystem = require('../../../lib/helper/FileSystem') -global.codeceptjs = require('../../../lib'); +global.codeceptjs = require('../../../lib') -let fs; +let fs describe('FileSystem', () => { before(() => { - global.codecept_dir = path.join(__dirname, '/../..'); - }); + global.codecept_dir = path.join(__dirname, '/../..') + }) beforeEach(() => { - fs = new FileSystem(); - fs._before(); - }); + fs = new FileSystem() + fs._before() + }) it('should be initialized before tests', () => { - expect(fs.dir).to.eql(global.codecept_dir); - }); + expect(fs.dir).to.eql(global.codecept_dir) + }) it('should open dirs', () => { - fs.amInPath('data'); - expect(fs.dir).to.eql(path.join(global.codecept_dir, '/data')); - }); + fs.amInPath('data') + expect(fs.dir).to.eql(path.join(global.codecept_dir, '/data')) + }) it('should see file', () => { - fs.seeFile('data/fs_sample.txt'); - fs.amInPath('data'); - fs.seeFile('fs_sample.txt'); - expect(fs.grabFileNames()).to.include('fs_sample.txt'); - fs.seeFileNameMatching('sample'); - }); + fs.seeFile('data/fs_sample.txt') + fs.amInPath('data') + fs.seeFile('fs_sample.txt') + expect(fs.grabFileNames()).to.include('fs_sample.txt') + fs.seeFileNameMatching('sample') + }) it('should check file contents', () => { - fs.seeFile('data/fs_sample.txt'); - fs.seeInThisFile('FileSystem'); - fs.dontSeeInThisFile('WebDriverIO'); - fs.dontSeeFileContentsEqual('123345'); + fs.seeFile('data/fs_sample.txt') + fs.seeInThisFile('FileSystem') + fs.dontSeeInThisFile('WebDriverIO') + fs.dontSeeFileContentsEqual('123345') fs.seeFileContentsEqual(`A simple file for FileSystem helper -test`); - }); +test`) + }) it('should write text to file', () => { - const outputFilePath = 'data/output/fs_output.txt'; - const text = '123'; - fs.writeToFile(outputFilePath, text); - fs.seeFile(outputFilePath); - fs.seeInThisFile(text); - }); -}); + const outputFilePath = 'data/output/fs_output.txt' + const text = '123' + fs.writeToFile(outputFilePath, text) + fs.seeFile(outputFilePath) + fs.seeInThisFile(text) + }) +}) diff --git a/test/unit/helper/element_not_found_test.js b/test/unit/helper/element_not_found_test.js index f09b92680..7a19e063e 100644 --- a/test/unit/helper/element_not_found_test.js +++ b/test/unit/helper/element_not_found_test.js @@ -1,35 +1,31 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const ElementNotFound = require('../../../lib/helper/errors/ElementNotFound'); +const ElementNotFound = require('../../../lib/helper/errors/ElementNotFound') -const locator = '#invalidSelector'; +const locator = '#invalidSelector' describe('ElementNotFound error', () => { it('should throw error', () => { - expect(() => new ElementNotFound(locator)).to.throw(Error); - }); + expect(() => new ElementNotFound(locator)).to.throw(Error) + }) it('should provide default message', () => { - expect(() => new ElementNotFound(locator)) - .to.throw(Error, 'Element "#invalidSelector" was not found by text|CSS|XPath'); - }); + expect(() => new ElementNotFound(locator)).to.throw(Error, 'Element "#invalidSelector" was not found by text|CSS|XPath') + }) it('should use prefix for message', () => { - expect(() => new ElementNotFound(locator, 'Field')) - .to.throw(Error, 'Field "#invalidSelector" was not found by text|CSS|XPath'); - }); + expect(() => new ElementNotFound(locator, 'Field')).to.throw(Error, 'Field "#invalidSelector" was not found by text|CSS|XPath') + }) it('should use postfix for message', () => { - expect(() => new ElementNotFound(locator, 'Locator', 'cannot be found')) - .to.throw(Error, 'Locator "#invalidSelector" cannot be found'); - }); + expect(() => new ElementNotFound(locator, 'Locator', 'cannot be found')).to.throw(Error, 'Locator "#invalidSelector" cannot be found') + }) it('should stringify locator object', () => { - const objectLocator = { css: locator }; - expect(() => new ElementNotFound(objectLocator)) - .to.throw(Error, `Element "${JSON.stringify(objectLocator)}" was not found by text|CSS|XPath`); - }); -}); + const objectLocator = { css: locator } + expect(() => new ElementNotFound(objectLocator)).to.throw(Error, `Element "${JSON.stringify(objectLocator)}" was not found by text|CSS|XPath`) + }) +}) diff --git a/test/unit/plugin/customLocator_test.js b/test/unit/plugin/customLocator_test.js index e3834c7f8..aab33ff08 100644 --- a/test/unit/plugin/customLocator_test.js +++ b/test/unit/plugin/customLocator_test.js @@ -1,91 +1,91 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const customLocatorPlugin = require('../../../lib/plugin/customLocator'); -const Locator = require('../../../lib/locator'); + expect = chai.expect +}) +const customLocatorPlugin = require('../../../lib/plugin/customLocator') +const Locator = require('../../../lib/locator') describe('customLocator', () => { beforeEach(() => { - Locator.filters = []; - }); + Locator.filters = [] + }) it('add a custom locator by $ -> data-qa', () => { customLocatorPlugin({ prefix: '$', attribute: 'data-qa', showActual: true, - }); - const l = new Locator('$user-id'); - expect(l.isXPath()).to.be.true; - expect(l.toXPath()).to.eql('.//*[@data-qa=\'user-id\']'); - expect(l.toString()).to.eql('.//*[@data-qa=\'user-id\']'); - }); + }) + const l = new Locator('$user-id') + expect(l.isXPath()).to.be.true + expect(l.toXPath()).to.eql(".//*[@data-qa='user-id']") + expect(l.toString()).to.eql(".//*[@data-qa='user-id']") + }) it('add a custom locator by = -> data-test-id', () => { customLocatorPlugin({ prefix: '=', attribute: 'data-test-id', showActual: false, - }); - const l = new Locator('=no-user'); - expect(l.isXPath()).to.be.true; - expect(l.toXPath()).to.eql('.//*[@data-test-id=\'no-user\']'); - expect(l.toString()).to.eql('=no-user'); - }); + }) + const l = new Locator('=no-user') + expect(l.isXPath()).to.be.true + expect(l.toXPath()).to.eql(".//*[@data-test-id='no-user']") + expect(l.toString()).to.eql('=no-user') + }) it('add a custom locator with multple char prefix = -> data-test-id', () => { customLocatorPlugin({ prefix: 'test=', attribute: 'data-test-id', showActual: false, - }); - const l = new Locator('test=no-user'); - expect(l.isXPath()).to.be.true; - expect(l.toXPath()).to.eql('.//*[@data-test-id=\'no-user\']'); - expect(l.toString()).to.eql('test=no-user'); - }); + }) + const l = new Locator('test=no-user') + expect(l.isXPath()).to.be.true + expect(l.toXPath()).to.eql(".//*[@data-test-id='no-user']") + expect(l.toString()).to.eql('test=no-user') + }) it('add a custom locator with CSS', () => { customLocatorPlugin({ prefix: '$', attribute: 'data-test', strategy: 'css', - }); - const l = new Locator('$user'); - expect(l.isCSS()).to.be.true; - expect(l.simplify()).to.eql('[data-test=user]'); - }); + }) + const l = new Locator('$user') + expect(l.isCSS()).to.be.true + expect(l.simplify()).to.eql('[data-test=user]') + }) it('add a custom locator with array $ -> data-qa, data-qa-id', () => { customLocatorPlugin({ prefix: '$', attribute: ['data-qa', 'data-qa-id'], showActual: true, - }); - const l = new Locator('$user-id'); - expect(l.isXPath()).to.be.true; - expect(l.toXPath()).to.eql('.//*[@data-qa=\'user-id\' or @data-qa-id=\'user-id\']'); - expect(l.toString()).to.eql('.//*[@data-qa=\'user-id\' or @data-qa-id=\'user-id\']'); - }); + }) + const l = new Locator('$user-id') + expect(l.isXPath()).to.be.true + expect(l.toXPath()).to.eql(".//*[@data-qa='user-id' or @data-qa-id='user-id']") + expect(l.toString()).to.eql(".//*[@data-qa='user-id' or @data-qa-id='user-id']") + }) it('add a custom locator array with CSS', () => { customLocatorPlugin({ prefix: '$', attribute: ['data-test', 'data-test-id'], strategy: 'css', - }); - const l = new Locator('$user'); - expect(l.isCSS()).to.be.true; - expect(l.simplify()).to.eql('[data-test=user],[data-test-id=user]'); - }); + }) + const l = new Locator('$user') + expect(l.isCSS()).to.be.true + expect(l.simplify()).to.eql('[data-test=user],[data-test-id=user]') + }) it('should return initial locator value when it does not start with specified prefix', () => { customLocatorPlugin({ prefix: '$', attribute: 'data-test', - }); - const l = new Locator('=user'); - expect(l.simplify()).to.eql('=user'); - }); -}); + }) + const l = new Locator('=user') + expect(l.simplify()).to.eql('=user') + }) +}) diff --git a/test/unit/plugin/eachElement_test.js b/test/unit/plugin/eachElement_test.js index efa6d19f3..1c7724156 100644 --- a/test/unit/plugin/eachElement_test.js +++ b/test/unit/plugin/eachElement_test.js @@ -1,49 +1,49 @@ -const path = require('path'); +const path = require('path') -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const container = require('../../../lib/container'); -const eachElement = require('../../../lib/plugin/eachElement')(); -const recorder = require('../../../lib/recorder'); + expect = chai.expect +}) +const container = require('../../../lib/container') +const eachElement = require('../../../lib/plugin/eachElement')() +const recorder = require('../../../lib/recorder') describe('eachElement plugin', () => { beforeEach(() => { - global.codecept_dir = path.join(__dirname, '/../..'); - recorder.start(); + global.codecept_dir = path.join(__dirname, '/../..') + recorder.start() container.create({ helpers: { MyHelper: { require: './data/helper', }, }, - }); - }); + }) + }) afterEach(() => { - container.clear(); - }); + container.clear() + }) it('should iterate for each elements', async () => { - let counter = 0; - await eachElement('some action', 'some locator', async (el) => { - expect(el).is.not.null; - counter++; - }); - await recorder.promise(); - expect(counter).to.equal(2); - }); + let counter = 0 + await eachElement('some action', 'some locator', async el => { + expect(el).is.not.null + counter++ + }) + await recorder.promise() + expect(counter).to.equal(2) + }) it('should not allow non async function', async () => { - let errorCaught = false; + let errorCaught = false try { - await eachElement('some action', 'some locator', (el) => {}); - await recorder.promise(); + await eachElement('some action', 'some locator', el => {}) + await recorder.promise() } catch (err) { - errorCaught = true; - expect(err.message).to.include('Async'); + errorCaught = true + expect(err.message).to.include('Async') } - expect(errorCaught).is.true; - }); -}); + expect(errorCaught).is.true + }) +}) diff --git a/test/unit/plugin/retryFailedStep_test.js b/test/unit/plugin/retryFailedStep_test.js index a866a94b3..2a13e2212 100644 --- a/test/unit/plugin/retryFailedStep_test.js +++ b/test/unit/plugin/retryFailedStep_test.js @@ -1,213 +1,274 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); + expect = chai.expect +}) -const retryFailedStep = require('../../../lib/plugin/retryFailedStep'); -const tryTo = require('../../../lib/plugin/tryTo'); -const within = require('../../../lib/within'); -const session = require('../../../lib/session'); -const container = require('../../../lib/container'); -const event = require('../../../lib/event'); -const recorder = require('../../../lib/recorder'); +const retryFailedStep = require('../../../lib/plugin/retryFailedStep') +const tryTo = require('../../../lib/plugin/tryTo') +const within = require('../../../lib/within') +const session = require('../../../lib/session') +const container = require('../../../lib/container') +const event = require('../../../lib/event') +const recorder = require('../../../lib/recorder') describe('retryFailedStep', () => { beforeEach(() => { - recorder.retries = []; + recorder.retries = [] container.clear({ mock: { _session: () => {}, }, - }); - recorder.start(); - }); + }) + recorder.start() + }) afterEach(() => { - event.dispatcher.emit(event.step.finished, { }); - }); + event.dispatcher.emit(event.step.finished, {}) + }) it('should retry failed step', async () => { - retryFailedStep({ retries: 2, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); - event.dispatcher.emit(event.step.started, { name: 'click' }); - - let counter = 0; - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error(); - } - }, undefined, undefined, true); - return recorder.promise(); - }); + retryFailedStep({ retries: 2, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.step.started, { name: 'click' }) + + let counter = 0 + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error() + } + }, + undefined, + undefined, + true, + ) + return recorder.promise() + }) it('should not retry failed step when tryTo plugin is enabled', async () => { - tryTo(); - retryFailedStep({ retries: 2, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); - event.dispatcher.emit(event.step.started, { name: 'click' }); + tryTo() + retryFailedStep({ retries: 2, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.step.started, { name: 'click' }) try { - let counter = 0; - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error('Retry failed step is disabled when tryTo plugin is enabled'); - } - }, undefined, undefined, true); - return recorder.promise(); + let counter = 0 + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error('Retry failed step is disabled when tryTo plugin is enabled') + } + }, + undefined, + undefined, + true, + ) + return recorder.promise() } catch (e) { - expect(e.message).equal('Retry failed step is disabled when tryTo plugin is enabled'); + expect(e.message).equal('Retry failed step is disabled when tryTo plugin is enabled') } - }); + }) it('should not retry within', async () => { - retryFailedStep({ retries: 1, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); + retryFailedStep({ retries: 1, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) - let counter = 0; - event.dispatcher.emit(event.step.started, { name: 'click' }); + let counter = 0 + event.dispatcher.emit(event.step.started, { name: 'click' }) try { within('foo', () => { - recorder.add(() => { - counter++; - throw new Error(); - }, undefined, undefined, true); - }); - await recorder.promise(); + recorder.add( + () => { + counter++ + throw new Error() + }, + undefined, + undefined, + true, + ) + }) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } - expect(process.env.FAILED_STEP_RETRIES).to.equal('1'); + expect(process.env.FAILED_STEP_RETRIES).to.equal('1') // expects to retry only once - counter.should.equal(2); - }); + counter.should.equal(2) + }) it('should not retry steps with wait*', async () => { - retryFailedStep({ retries: 2, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); + retryFailedStep({ retries: 2, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) - let counter = 0; - event.dispatcher.emit(event.step.started, { name: 'waitForElement' }); + let counter = 0 + event.dispatcher.emit(event.step.started, { name: 'waitForElement' }) try { - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error(); - } - }, undefined, undefined, true); - await recorder.promise(); + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error() + } + }, + undefined, + undefined, + true, + ) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } - expect(counter).to.equal(1); + expect(counter).to.equal(1) // expects to retry only once - }); + }) it('should not retry steps with amOnPage', async () => { - retryFailedStep({ retries: 2, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); + retryFailedStep({ retries: 2, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) - let counter = 0; - event.dispatcher.emit(event.step.started, { name: 'amOnPage' }); + let counter = 0 + event.dispatcher.emit(event.step.started, { name: 'amOnPage' }) try { - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error(); - } - }, undefined, undefined, true); - await recorder.promise(); + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error() + } + }, + undefined, + undefined, + true, + ) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } - expect(counter).to.equal(1); + expect(counter).to.equal(1) // expects to retry only once - }); + }) it('should add custom steps to ignore', async () => { - retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: ['somethingNew*'] }); - event.dispatcher.emit(event.test.before, {}); + retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: ['somethingNew*'] }) + event.dispatcher.emit(event.test.before, {}) - let counter = 0; - event.dispatcher.emit(event.step.started, { name: 'somethingNew' }); + let counter = 0 + event.dispatcher.emit(event.step.started, { name: 'somethingNew' }) try { - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error(); - } - }, undefined, undefined, true); - await recorder.promise(); + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error() + } + }, + undefined, + undefined, + true, + ) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } - expect(counter).to.equal(1); + expect(counter).to.equal(1) // expects to retry only once - }); + }) it('should add custom regexp steps to ignore', async () => { - retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: [/somethingNew/] }); - event.dispatcher.emit(event.test.before, {}); + retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: [/somethingNew/] }) + event.dispatcher.emit(event.test.before, {}) - let counter = 0; - event.dispatcher.emit(event.step.started, { name: 'somethingNew' }); + let counter = 0 + event.dispatcher.emit(event.step.started, { name: 'somethingNew' }) try { - await recorder.add(() => { - counter++; - if (counter < 3) { - throw new Error(); - } - }, undefined, undefined, true); - await recorder.promise(); + await recorder.add( + () => { + counter++ + if (counter < 3) { + throw new Error() + } + }, + undefined, + undefined, + true, + ) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } - expect(counter).to.equal(1); + expect(counter).to.equal(1) // expects to retry only once - }); + }) it('should not retry session', async () => { - retryFailedStep({ retries: 1, minTimeout: 1 }); - event.dispatcher.emit(event.test.before, {}); - event.dispatcher.emit(event.step.started, { name: 'click' }); - let counter = 0; + retryFailedStep({ retries: 1, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.step.started, { name: 'click' }) + let counter = 0 try { session('foo', () => { - recorder.add(() => { - counter++; - throw new Error(); - }, undefined, undefined, true); - }); - await recorder.promise(); + recorder.add( + () => { + counter++ + throw new Error() + }, + undefined, + undefined, + true, + ) + }) + await recorder.promise() } catch (e) { - await recorder.catchWithoutStop((err) => err); + await recorder.catchWithoutStop(err => err) } // expects to retry only once - expect(counter).to.equal(2); - }); + expect(counter).to.equal(2) + }) it('should not turn around the chain of retries', () => { - recorder.retry({ retries: 2, when: (err) => { return err.message === 'someerror'; }, identifier: 'test' }); - recorder.retry({ retries: 2, when: (err) => { return err.message === 'othererror'; } }); - - const getRetryIndex = () => recorder.retries.indexOf(recorder.retries.find(retry => retry.identifier)); - let initalIndex; - - recorder.add(() => { - initalIndex = getRetryIndex(); - }, undefined, undefined, true); - - recorder.add(() => { - initalIndex.should.equal(getRetryIndex()); - }, undefined, undefined, true); - return recorder.promise(); - }); -}); + recorder.retry({ + retries: 2, + when: err => { + return err.message === 'someerror' + }, + identifier: 'test', + }) + recorder.retry({ + retries: 2, + when: err => { + return err.message === 'othererror' + }, + }) + + const getRetryIndex = () => recorder.retries.indexOf(recorder.retries.find(retry => retry.identifier)) + let initalIndex + + recorder.add( + () => { + initalIndex = getRetryIndex() + }, + undefined, + undefined, + true, + ) + + recorder.add( + () => { + initalIndex.should.equal(getRetryIndex()) + }, + undefined, + undefined, + true, + ) + return recorder.promise() + }) +}) diff --git a/test/unit/plugin/retryto_test.js b/test/unit/plugin/retryto_test.js index 293adf1d7..213dd577b 100644 --- a/test/unit/plugin/retryto_test.js +++ b/test/unit/plugin/retryto_test.js @@ -1,36 +1,42 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const retryTo = require('../../../lib/plugin/retryTo')(); -const recorder = require('../../../lib/recorder'); + expect = chai.expect +}) +const retryTo = require('../../../lib/plugin/retryTo')() +const recorder = require('../../../lib/recorder') describe('retryTo plugin', () => { beforeEach(() => { - recorder.start(); - }); + recorder.start() + }) it('should execute command on success', async () => { - let counter = 0; - await retryTo(() => recorder.add(() => counter++), 5); - expect(counter).is.equal(1); - return recorder.promise(); - }); + let counter = 0 + await retryTo(() => recorder.add(() => counter++), 5) + expect(counter).is.equal(1) + return recorder.promise() + }) it('should execute few times command on fail', async () => { - let counter = 0; - let errorCaught = false; + let counter = 0 + let errorCaught = false try { - await retryTo(() => { - recorder.add(() => counter++); - recorder.add(() => { throw new Error('Ups'); }); - }, 5, 10); - await recorder.promise(); + await retryTo( + () => { + recorder.add(() => counter++) + recorder.add(() => { + throw new Error('Ups') + }) + }, + 5, + 10, + ) + await recorder.promise() } catch (err) { - errorCaught = true; - expect(err.message).to.eql('Ups'); + errorCaught = true + expect(err.message).to.eql('Ups') } - expect(counter).to.equal(5); - expect(errorCaught).is.true; - }); -}); + expect(counter).to.equal(5) + expect(errorCaught).is.true + }) +}) diff --git a/test/unit/plugin/screenshotOnFail_test.js b/test/unit/plugin/screenshotOnFail_test.js index fc6b7295f..70d07e501 100644 --- a/test/unit/plugin/screenshotOnFail_test.js +++ b/test/unit/plugin/screenshotOnFail_test.js @@ -1,82 +1,82 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const sinon = require('sinon'); + expect = chai.expect +}) +const sinon = require('sinon') -const screenshotOnFail = require('../../../lib/plugin/screenshotOnFail'); -const container = require('../../../lib/container'); -const event = require('../../../lib/event'); -const recorder = require('../../../lib/recorder'); +const screenshotOnFail = require('../../../lib/plugin/screenshotOnFail') +const container = require('../../../lib/container') +const event = require('../../../lib/event') +const recorder = require('../../../lib/recorder') -let screenshotSaved; +let screenshotSaved describe('screenshotOnFail', () => { beforeEach(() => { - recorder.reset(); - screenshotSaved = sinon.spy(); + recorder.reset() + screenshotSaved = sinon.spy() container.clear({ WebDriver: { options: {}, saveScreenshot: screenshotSaved, }, - }); - }); + }) + }) it('should remove the . at the end of test title', async () => { - screenshotOnFail({}); - event.dispatcher.emit(event.test.failed, { title: 'test title.' }); - await recorder.promise(); - expect(screenshotSaved.called).is.ok; - expect('test_title.failed.png').is.equal(screenshotSaved.getCall(0).args[0]); - }); + screenshotOnFail({}) + event.dispatcher.emit(event.test.failed, { title: 'test title.' }) + await recorder.promise() + expect(screenshotSaved.called).is.ok + expect('test_title.failed.png').is.equal(screenshotSaved.getCall(0).args[0]) + }) it('should exclude the data driven in failed screenshot file name', async () => { - screenshotOnFail({}); - event.dispatcher.emit(event.test.failed, { title: 'Scenario with data driven | {"login":"admin","password":"123456"}' }); - await recorder.promise(); - expect(screenshotSaved.called).is.ok; - expect('Scenario_with_data_driven.failed.png').is.equal(screenshotSaved.getCall(0).args[0]); - }); + screenshotOnFail({}) + event.dispatcher.emit(event.test.failed, { title: 'Scenario with data driven | {"login":"admin","password":"123456"}' }) + await recorder.promise() + expect(screenshotSaved.called).is.ok + expect('Scenario_with_data_driven.failed.png').is.equal(screenshotSaved.getCall(0).args[0]) + }) it('should create screenshot on fail', async () => { - screenshotOnFail({}); - event.dispatcher.emit(event.test.failed, { title: 'test1' }); - await recorder.promise(); - expect(screenshotSaved.called).is.ok; - expect('test1.failed.png').is.equal(screenshotSaved.getCall(0).args[0]); - }); + screenshotOnFail({}) + event.dispatcher.emit(event.test.failed, { title: 'test1' }) + await recorder.promise() + expect(screenshotSaved.called).is.ok + expect('test1.failed.png').is.equal(screenshotSaved.getCall(0).args[0]) + }) it('should create screenshot with unique name', async () => { - screenshotOnFail({ uniqueScreenshotNames: true }); - event.dispatcher.emit(event.test.failed, { title: 'test1', uuid: 1 }); - await recorder.promise(); - expect(screenshotSaved.called).is.ok; - expect('test1_1.failed.png').is.equal(screenshotSaved.getCall(0).args[0]); - }); + screenshotOnFail({ uniqueScreenshotNames: true }) + event.dispatcher.emit(event.test.failed, { title: 'test1', uuid: 1 }) + await recorder.promise() + expect(screenshotSaved.called).is.ok + expect('test1_1.failed.png').is.equal(screenshotSaved.getCall(0).args[0]) + }) it('should create screenshot with unique name when uuid is null', async () => { - screenshotOnFail({ uniqueScreenshotNames: true }); - event.dispatcher.emit(event.test.failed, { title: 'test1' }); - await recorder.promise(); - expect(screenshotSaved.called).is.ok; - const fileName = screenshotSaved.getCall(0).args[0]; - const regexpFileName = /test1_[0-9]{10}.failed.png/; - expect(fileName.match(regexpFileName).length).is.equal(1); - }); + screenshotOnFail({ uniqueScreenshotNames: true }) + event.dispatcher.emit(event.test.failed, { title: 'test1' }) + await recorder.promise() + expect(screenshotSaved.called).is.ok + const fileName = screenshotSaved.getCall(0).args[0] + const regexpFileName = /test1_[0-9]{10}.failed.png/ + expect(fileName.match(regexpFileName).length).is.equal(1) + }) it('should not save screenshot in BeforeSuite', async () => { - screenshotOnFail({ uniqueScreenshotNames: true }); - event.dispatcher.emit(event.test.failed, { title: 'test1', ctx: { _runnable: { title: 'hook: BeforeSuite' } } }); - await recorder.promise(); - expect(!screenshotSaved.called).is.ok; - }); + screenshotOnFail({ uniqueScreenshotNames: true }) + event.dispatcher.emit(event.test.failed, { title: 'test1', ctx: { _runnable: { title: 'hook: BeforeSuite' } } }) + await recorder.promise() + expect(!screenshotSaved.called).is.ok + }) it('should not save screenshot in AfterSuite', async () => { - screenshotOnFail({ uniqueScreenshotNames: true }); - event.dispatcher.emit(event.test.failed, { title: 'test1', ctx: { _runnable: { title: 'hook: AfterSuite' } } }); - await recorder.promise(); - expect(!screenshotSaved.called).is.ok; - }); + screenshotOnFail({ uniqueScreenshotNames: true }) + event.dispatcher.emit(event.test.failed, { title: 'test1', ctx: { _runnable: { title: 'hook: AfterSuite' } } }) + await recorder.promise() + expect(!screenshotSaved.called).is.ok + }) // TODO: write more tests for different options -}); +}) diff --git a/test/unit/plugin/subtitles_test.js b/test/unit/plugin/subtitles_test.js index 64c59694c..a20b5459c 100644 --- a/test/unit/plugin/subtitles_test.js +++ b/test/unit/plugin/subtitles_test.js @@ -1,15 +1,15 @@ -const sinon = require('sinon'); +const sinon = require('sinon') -const fsPromises = require('fs').promises; -const subtitles = require('../../../lib/plugin/subtitles'); -const container = require('../../../lib/container'); -const event = require('../../../lib/event'); -const recorder = require('../../../lib/recorder'); +const fsPromises = require('fs').promises +const subtitles = require('../../../lib/plugin/subtitles') +const container = require('../../../lib/container') +const event = require('../../../lib/event') +const recorder = require('../../../lib/recorder') function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); + return new Promise(resolve => { + setTimeout(resolve, ms) + }) } describe('subtitles', () => { @@ -18,130 +18,149 @@ describe('subtitles', () => { mock: { _session: () => {}, }, - }); - recorder.start(); - }); + }) + recorder.start() + }) before(() => { - subtitles({}); - }); + subtitles({}) + }) it('should not capture subtitle as video artifact was missing', async () => { - const fsMock = sinon.mock(fsPromises); + const fsMock = sinon.mock(fsPromises) - const test = {}; + const test = {} - fsMock.expects('writeFile') - .never(); + fsMock.expects('writeFile').never() - event.dispatcher.emit(event.test.before, test); - const step1 = { name: 'see', actor: 'I', args: ['Test 1'] }; - event.dispatcher.emit(event.step.started, step1); - event.dispatcher.emit(event.step.finished, step1); - event.dispatcher.emit(event.test.after, test); - fsMock.verify(); - }); + event.dispatcher.emit(event.test.before, test) + const step1 = { name: 'see', actor: 'I', args: ['Test 1'] } + event.dispatcher.emit(event.step.started, step1) + event.dispatcher.emit(event.step.finished, step1) + event.dispatcher.emit(event.test.after, test) + fsMock.verify() + }) it('should capture subtitle as video artifact is present', async () => { - const fsMock = sinon.mock(fsPromises); + const fsMock = sinon.mock(fsPromises) const test = { artifacts: { video: '../../lib/output/failedTest1.webm', }, - }; + } - fsMock.expects('writeFile') + fsMock + .expects('writeFile') .once() - .withExactArgs('../../lib/output/failedTest1.srt', sinon.match((value) => { - return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n$/gm); - })); - - event.dispatcher.emit(event.test.before, test); - const step1 = { name: 'click', actor: 'I', args: ['Continue'] }; - event.dispatcher.emit(event.step.started, step1); - event.dispatcher.emit(event.step.finished, step1); - event.dispatcher.emit(event.test.after, test); - fsMock.verify(); - }); + .withExactArgs( + '../../lib/output/failedTest1.srt', + sinon.match(value => { + return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n$/gm) + }), + ) + + event.dispatcher.emit(event.test.before, test) + const step1 = { name: 'click', actor: 'I', args: ['Continue'] } + event.dispatcher.emit(event.step.started, step1) + event.dispatcher.emit(event.step.finished, step1) + event.dispatcher.emit(event.test.after, test) + fsMock.verify() + }) it('should capture mutiple steps as subtitle', async () => { - const fsMock = sinon.mock(fsPromises); + const fsMock = sinon.mock(fsPromises) const test = { artifacts: { video: '../../lib/output/failedTest1.webm', }, - }; + } - fsMock.expects('writeFile') + fsMock + .expects('writeFile') .once() - .withExactArgs('../../lib/output/failedTest1.srt', sinon.match((value) => { - return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n2\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.see\(Github\)\n\n$/gm); - })); - - event.dispatcher.emit(event.test.before, test); - const step1 = { name: 'click', actor: 'I', args: ['Continue'] }; - const step2 = { name: 'see', actor: 'I', args: ['Github'] }; - event.dispatcher.emit(event.step.started, step1); - event.dispatcher.emit(event.step.started, step2); - event.dispatcher.emit(event.step.finished, step2); - await sleep(300); - - event.dispatcher.emit(event.step.finished, step1); - event.dispatcher.emit(event.test.after, test); - fsMock.verify(); - }); + .withExactArgs( + '../../lib/output/failedTest1.srt', + sinon.match(value => { + return value.match( + /^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n2\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.see\(Github\)\n\n$/gm, + ) + }), + ) + + event.dispatcher.emit(event.test.before, test) + const step1 = { name: 'click', actor: 'I', args: ['Continue'] } + const step2 = { name: 'see', actor: 'I', args: ['Github'] } + event.dispatcher.emit(event.step.started, step1) + event.dispatcher.emit(event.step.started, step2) + event.dispatcher.emit(event.step.finished, step2) + await sleep(300) + + event.dispatcher.emit(event.step.finished, step1) + event.dispatcher.emit(event.test.after, test) + fsMock.verify() + }) it('should capture seperate steps for separate tests', async () => { - const fsMock = sinon.mock(fsPromises); + const fsMock = sinon.mock(fsPromises) const test1 = { artifacts: { video: '../../lib/output/failedTest1.webm', }, - }; + } - fsMock.expects('writeFile') + fsMock + .expects('writeFile') .once() - .withExactArgs('../../lib/output/failedTest1.srt', sinon.match((value) => { - return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n2\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.see\(Github\)\n\n$/gm); - })); - - event.dispatcher.emit(event.test.before, test1); - const step1 = { name: 'click', actor: 'I', args: ['Continue'] }; - const step2 = { name: 'see', actor: 'I', args: ['Github'] }; - event.dispatcher.emit(event.step.started, step1); - event.dispatcher.emit(event.step.started, step2); - event.dispatcher.emit(event.step.finished, step2); - await sleep(300); - - event.dispatcher.emit(event.step.finished, step1); - event.dispatcher.emit(event.test.after, test1); - fsMock.verify(); - fsMock.restore(); + .withExactArgs( + '../../lib/output/failedTest1.srt', + sinon.match(value => { + return value.match( + /^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Continue\)\n\n2\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.see\(Github\)\n\n$/gm, + ) + }), + ) + + event.dispatcher.emit(event.test.before, test1) + const step1 = { name: 'click', actor: 'I', args: ['Continue'] } + const step2 = { name: 'see', actor: 'I', args: ['Github'] } + event.dispatcher.emit(event.step.started, step1) + event.dispatcher.emit(event.step.started, step2) + event.dispatcher.emit(event.step.finished, step2) + await sleep(300) + + event.dispatcher.emit(event.step.finished, step1) + event.dispatcher.emit(event.test.after, test1) + fsMock.verify() + fsMock.restore() /** * To Ensure that when multiple tests are run steps are not incorrectly captured */ - const fsMock1 = sinon.mock(fsPromises); - fsMock1.expects('writeFile') + const fsMock1 = sinon.mock(fsPromises) + fsMock1 + .expects('writeFile') .once() - .withExactArgs('../../lib/output/failedTest2.srt', sinon.match((value) => { - return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Login\)\n\n$/gm); - })); + .withExactArgs( + '../../lib/output/failedTest2.srt', + sinon.match(value => { + return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Login\)\n\n$/gm) + }), + ) const test2 = { artifacts: { video: '../../lib/output/failedTest2.webm', }, - }; - - event.dispatcher.emit(event.test.before, test2); - const step3 = { name: 'click', actor: 'I', args: ['Login'] }; - event.dispatcher.emit(event.step.started, step3); - await sleep(300); - - event.dispatcher.emit(event.step.finished, step3); - event.dispatcher.emit(event.test.after, test2); - fsMock1.verify(); - }); -}); + } + + event.dispatcher.emit(event.test.before, test2) + const step3 = { name: 'click', actor: 'I', args: ['Login'] } + event.dispatcher.emit(event.step.started, step3) + await sleep(300) + + event.dispatcher.emit(event.step.finished, step3) + event.dispatcher.emit(event.test.after, test2) + fsMock1.verify() + }) +}) diff --git a/test/unit/plugin/tryTo_test.js b/test/unit/plugin/tryTo_test.js index 412f0ccb8..efd8ebdea 100644 --- a/test/unit/plugin/tryTo_test.js +++ b/test/unit/plugin/tryTo_test.js @@ -1,26 +1,28 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const tryTo = require('../../../lib/plugin/tryTo')(); -const recorder = require('../../../lib/recorder'); + expect = chai.expect +}) +const tryTo = require('../../../lib/plugin/tryTo')() +const recorder = require('../../../lib/recorder') describe('tryTo plugin', () => { beforeEach(() => { - recorder.start(); - }); + recorder.start() + }) it('should execute command on success', async () => { - const ok = await tryTo(() => recorder.add(() => 5)); - expect(true).is.equal(ok); - return recorder.promise(); - }); + const ok = await tryTo(() => recorder.add(() => 5)) + expect(true).is.equal(ok) + return recorder.promise() + }) it('should execute command on fail', async () => { - const notOk = await tryTo(() => recorder.add(() => { - throw new Error('Ups'); - })); - expect(false).is.equal(notOk); - return recorder.promise(); - }); -}); + const notOk = await tryTo(() => + recorder.add(() => { + throw new Error('Ups') + }), + ) + expect(false).is.equal(notOk) + return recorder.promise() + }) +}) diff --git a/test/unit/scenario_test.js b/test/unit/scenario_test.js index c97cf91a9..601956989 100644 --- a/test/unit/scenario_test.js +++ b/test/unit/scenario_test.js @@ -1,99 +1,99 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const sinon = require('sinon'); + expect = chai.expect +}) +const sinon = require('sinon') -const scenario = require('../../lib/mocha/scenario'); -const recorder = require('../../lib/recorder'); -const event = require('../../lib/event'); +const scenario = require('../../lib/mocha/scenario') +const recorder = require('../../lib/recorder') +const event = require('../../lib/event') -let test; -let fn; -let before; -let after; -let beforeSuite; -let afterSuite; -let failed; -let started; +let test +let fn +let before +let after +let beforeSuite +let afterSuite +let failed +let started describe('Scenario', () => { beforeEach(() => { - test = { timeout: () => {} }; - fn = sinon.spy(); - test.fn = fn; - }); - beforeEach(() => recorder.reset()); - afterEach(() => event.cleanDispatcher()); + test = { timeout: () => {} } + fn = sinon.spy() + test.fn = fn + }) + beforeEach(() => recorder.reset()) + afterEach(() => event.cleanDispatcher()) it('should wrap test function', () => { - scenario.test(test).fn(() => {}); - expect(fn.called).is.ok; - }); + scenario.test(test).fn(() => {}) + expect(fn.called).is.ok + }) it('should work with async func', () => { - let counter = 0; + let counter = 0 test.fn = () => { recorder.add('test', async () => { - await counter++; - await counter++; - await counter++; - counter++; - }); - }; + await counter++ + await counter++ + await counter++ + counter++ + }) + } - scenario.setup(); - scenario.test(test).fn(() => null); - recorder.add('validation', () => expect(counter).to.eq(4)); - return recorder.promise(); - }); + scenario.setup() + scenario.test(test).fn(() => null) + recorder.add('validation', () => expect(counter).to.eq(4)) + return recorder.promise() + }) describe('events', () => { beforeEach(() => { - event.dispatcher.on(event.test.before, (before = sinon.spy())); - event.dispatcher.on(event.test.after, (after = sinon.spy())); - event.dispatcher.on(event.test.started, (started = sinon.spy())); - event.dispatcher.on(event.suite.before, (beforeSuite = sinon.spy())); - event.dispatcher.on(event.suite.after, (afterSuite = sinon.spy())); - scenario.suiteSetup(); - scenario.setup(); - }); + event.dispatcher.on(event.test.before, (before = sinon.spy())) + event.dispatcher.on(event.test.after, (after = sinon.spy())) + event.dispatcher.on(event.test.started, (started = sinon.spy())) + event.dispatcher.on(event.suite.before, (beforeSuite = sinon.spy())) + event.dispatcher.on(event.suite.after, (afterSuite = sinon.spy())) + scenario.suiteSetup() + scenario.setup() + }) it('should fire events', () => { - scenario.test(test).fn(() => null); - expect(started.called).is.ok; - scenario.teardown(); - scenario.suiteTeardown(); + scenario.test(test).fn(() => null) + expect(started.called).is.ok + scenario.teardown() + scenario.suiteTeardown() return recorder .promise() .then(() => expect(beforeSuite.called).is.ok) .then(() => expect(afterSuite.called).is.ok) .then(() => expect(before.called).is.ok) - .then(() => expect(after.called).is.ok); - }); + .then(() => expect(after.called).is.ok) + }) it('should fire failed event on error', () => { - event.dispatcher.on(event.test.failed, (failed = sinon.spy())); - scenario.setup(); + event.dispatcher.on(event.test.failed, (failed = sinon.spy())) + scenario.setup() test.fn = () => { - throw new Error('ups'); - }; - scenario.test(test).fn(() => {}); + throw new Error('ups') + } + scenario.test(test).fn(() => {}) return recorder .promise() .then(() => expect(failed.called).is.ok) - .catch(() => null); - }); + .catch(() => null) + }) it('should fire failed event on async error', () => { test.fn = () => { - recorder.throw(new Error('ups')); - }; - scenario.test(test).fn(() => {}); + recorder.throw(new Error('ups')) + } + scenario.test(test).fn(() => {}) return recorder .promise() .then(() => expect(failed.called).is.ok) - .catch(() => null); - }); - }); -}); + .catch(() => null) + }) + }) +}) diff --git a/test/unit/ui_test.js b/test/unit/ui_test.js index eb1bf5889..85034f775 100644 --- a/test/unit/ui_test.js +++ b/test/unit/ui_test.js @@ -1,220 +1,220 @@ -let expect; +let expect import('chai').then(chai => { - expect = chai.expect; -}); -const Mocha = require('mocha/lib/mocha'); -const Suite = require('mocha/lib/suite'); + expect = chai.expect +}) +const Mocha = require('mocha/lib/mocha') +const Suite = require('mocha/lib/suite') -global.codeceptjs = require('../../lib'); -const makeUI = require('../../lib/mocha/ui'); +global.codeceptjs = require('../../lib') +const makeUI = require('../../lib/mocha/ui') describe('ui', () => { - let suite; - let context; + let suite + let context beforeEach(() => { - context = {}; - suite = new Suite('empty'); - makeUI(suite); - suite.emit('pre-require', context, {}, new Mocha()); - }); + context = {} + suite = new Suite('empty') + makeUI(suite) + suite.emit('pre-require', context, {}, new Mocha()) + }) describe('basic constants', () => { - const constants = ['Before', 'Background', 'BeforeAll', 'After', 'AfterAll', 'Scenario', 'xScenario']; + const constants = ['Before', 'Background', 'BeforeAll', 'After', 'AfterAll', 'Scenario', 'xScenario'] constants.forEach(c => { - it(`context should contain ${c}`, () => expect(context[c]).is.ok); - }); - }); + it(`context should contain ${c}`, () => expect(context[c]).is.ok) + }) + }) describe('Feature', () => { - let suiteConfig; + let suiteConfig it('Feature should return featureConfig', () => { - suiteConfig = context.Feature('basic suite'); - expect(suiteConfig.suite).is.ok; - }); + suiteConfig = context.Feature('basic suite') + expect(suiteConfig.suite).is.ok + }) it('should contain title', () => { - suiteConfig = context.Feature('basic suite'); - expect(suiteConfig.suite).is.ok; - expect(suiteConfig.suite.title).eq('basic suite'); - expect(suiteConfig.suite.fullTitle()).eq('basic suite:'); - }); + suiteConfig = context.Feature('basic suite') + expect(suiteConfig.suite).is.ok + expect(suiteConfig.suite.title).eq('basic suite') + expect(suiteConfig.suite.fullTitle()).eq('basic suite:') + }) it('should contain tags', () => { - suiteConfig = context.Feature('basic suite'); - expect(0).eq(suiteConfig.suite.tags.length); + suiteConfig = context.Feature('basic suite') + expect(0).eq(suiteConfig.suite.tags.length) - suiteConfig = context.Feature('basic suite @very @important'); - expect(suiteConfig.suite).is.ok; + suiteConfig = context.Feature('basic suite @very @important') + expect(suiteConfig.suite).is.ok - suiteConfig.suite.tags.should.include('@very'); - suiteConfig.suite.tags.should.include('@important'); + suiteConfig.suite.tags.should.include('@very') + suiteConfig.suite.tags.should.include('@important') - suiteConfig.tag('@user'); - suiteConfig.suite.tags.should.include('@user'); + suiteConfig.tag('@user') + suiteConfig.suite.tags.should.include('@user') - suiteConfig.suite.tags.should.not.include('@slow'); - suiteConfig.tag('slow'); - suiteConfig.suite.tags.should.include('@slow'); - }); + suiteConfig.suite.tags.should.not.include('@slow') + suiteConfig.tag('slow') + suiteConfig.suite.tags.should.include('@slow') + }) it('retries can be set', () => { - suiteConfig = context.Feature('basic suite'); - suiteConfig.retry(3); - expect(3).eq(suiteConfig.suite.retries()); - }); + suiteConfig = context.Feature('basic suite') + suiteConfig.retry(3) + expect(3).eq(suiteConfig.suite.retries()) + }) it('timeout can be set', () => { - suiteConfig = context.Feature('basic suite'); - expect(0).eq(suiteConfig.suite.timeout()); - suiteConfig.timeout(3); - expect(3).eq(suiteConfig.suite.timeout()); - }); + suiteConfig = context.Feature('basic suite') + expect(0).eq(suiteConfig.suite.timeout()) + suiteConfig.timeout(3) + expect(3).eq(suiteConfig.suite.timeout()) + }) it('helpers can be configured', () => { - suiteConfig = context.Feature('basic suite'); - expect(!suiteConfig.suite.config); - suiteConfig.config('WebDriver', { browser: 'chrome' }); - expect('chrome').eq(suiteConfig.suite.config.WebDriver.browser); - suiteConfig.config({ browser: 'firefox' }); - expect('firefox').eq(suiteConfig.suite.config[0].browser); + suiteConfig = context.Feature('basic suite') + expect(!suiteConfig.suite.config) + suiteConfig.config('WebDriver', { browser: 'chrome' }) + expect('chrome').eq(suiteConfig.suite.config.WebDriver.browser) + suiteConfig.config({ browser: 'firefox' }) + expect('firefox').eq(suiteConfig.suite.config[0].browser) suiteConfig.config('WebDriver', () => { - return { browser: 'edge' }; - }); - expect('edge').eq(suiteConfig.suite.config.WebDriver.browser); - }); + return { browser: 'edge' } + }) + expect('edge').eq(suiteConfig.suite.config.WebDriver.browser) + }) it('Feature can be skipped', () => { - suiteConfig = context.Feature.skip('skipped suite'); - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); - }); + suiteConfig = context.Feature.skip('skipped suite') + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') + }) it('Feature can be skipped via xFeature', () => { - suiteConfig = context.xFeature('skipped suite'); - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); - }); + suiteConfig = context.xFeature('skipped suite') + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') + }) it('Feature are not skipped by default', () => { - suiteConfig = context.Feature('not skipped suite'); - expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true'); + suiteConfig = context.Feature('not skipped suite') + expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true') // expect(suiteConfig.suite.opts, undefined, 'Features should have no skip info'); - }); + }) it('Feature can be skipped', () => { - suiteConfig = context.Feature.skip('skipped suite'); - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); - }); + suiteConfig = context.Feature.skip('skipped suite') + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') + }) it('Feature can be skipped via xFeature', () => { - suiteConfig = context.xFeature('skipped suite'); - expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true'); - expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.'); - expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo'); - }); + suiteConfig = context.xFeature('skipped suite') + expect(suiteConfig.suite.pending).eq(true, 'Skipped Feature must be contain pending === true') + expect(suiteConfig.suite.opts.skipInfo.message).eq('Skipped due to "skip" on Feature.') + expect(suiteConfig.suite.opts.skipInfo.skipped).eq(true, 'Skip should be set on skipInfo') + }) it('Feature are not skipped by default', () => { - suiteConfig = context.Feature('not skipped suite'); - expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true'); - expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info'); - }); + suiteConfig = context.Feature('not skipped suite') + expect(suiteConfig.suite.pending).eq(false, 'Feature must not contain pending === true') + expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info') + }) it('Feature should correctly pass options to suite context', () => { - suiteConfig = context.Feature('not skipped suite', { key: 'value' }); - expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options'); - }); - }); + suiteConfig = context.Feature('not skipped suite', { key: 'value' }) + expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options') + }) + }) describe('Scenario', () => { - let scenarioConfig; + let scenarioConfig it('Scenario should return scenarioConfig', () => { - scenarioConfig = context.Scenario('basic scenario'); - expect(scenarioConfig.test).is.ok; - }); + scenarioConfig = context.Scenario('basic scenario') + expect(scenarioConfig.test).is.ok + }) it('should contain title', () => { - context.Feature('suite'); - scenarioConfig = context.Scenario('scenario'); - expect(scenarioConfig.test.title).eq('scenario'); - expect(scenarioConfig.test.fullTitle()).eq('suite: scenario'); - expect(scenarioConfig.test.tags.length).eq(0); - }); + context.Feature('suite') + scenarioConfig = context.Scenario('scenario') + expect(scenarioConfig.test.title).eq('scenario') + expect(scenarioConfig.test.fullTitle()).eq('suite: scenario') + expect(scenarioConfig.test.tags.length).eq(0) + }) it('should contain tags', () => { - context.Feature('basic suite @cool'); + context.Feature('basic suite @cool') - scenarioConfig = context.Scenario('scenario @very @important'); + scenarioConfig = context.Scenario('scenario @very @important') - scenarioConfig.test.tags.should.include('@cool'); - scenarioConfig.test.tags.should.include('@very'); - scenarioConfig.test.tags.should.include('@important'); + scenarioConfig.test.tags.should.include('@cool') + scenarioConfig.test.tags.should.include('@very') + scenarioConfig.test.tags.should.include('@important') - scenarioConfig.tag('@user'); - scenarioConfig.test.tags.should.include('@user'); - }); + scenarioConfig.tag('@user') + scenarioConfig.test.tags.should.include('@user') + }) it('should dynamically inject dependencies', () => { - scenarioConfig = context.Scenario('scenario'); - scenarioConfig.injectDependencies({ Data: 'data' }); - expect(scenarioConfig.test.inject.Data).eq('data'); - }); + scenarioConfig = context.Scenario('scenario') + scenarioConfig.injectDependencies({ Data: 'data' }) + expect(scenarioConfig.test.inject.Data).eq('data') + }) describe('todo', () => { it('should inject skipInfo to opts', () => { scenarioConfig = context.Scenario.todo('scenario', () => { - console.log('Scenario Body'); - }); + console.log('Scenario Body') + }) - expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true'); - expect(scenarioConfig.test.opts.skipInfo.message).eq('Test not implemented!'); - expect(scenarioConfig.test.opts.skipInfo.description).to.include("console.log('Scenario Body')"); - }); + expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true') + expect(scenarioConfig.test.opts.skipInfo.message).eq('Test not implemented!') + expect(scenarioConfig.test.opts.skipInfo.description).to.include("console.log('Scenario Body')") + }) it('should contain empty description in skipInfo and empty body', () => { - scenarioConfig = context.Scenario.todo('scenario'); + scenarioConfig = context.Scenario.todo('scenario') - expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true'); - expect(scenarioConfig.test.opts.skipInfo.description).eq(''); - expect(scenarioConfig.test.body).eq(''); - }); + expect(scenarioConfig.test.pending).eq(true, 'Todo Scenario must be contain pending === true') + expect(scenarioConfig.test.opts.skipInfo.description).eq('') + expect(scenarioConfig.test.body).eq('') + }) it('should inject custom opts to opts and without callback', () => { - scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }); + scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }) - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); - }); + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') + }) it('should inject custom opts to opts and with callback', () => { scenarioConfig = context.Scenario.todo('scenario', { customOpts: 'Custom Opts' }, () => { - console.log('Scenario Body'); - }); + console.log('Scenario Body') + }) - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); - }); - }); + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') + }) + }) describe('skip', () => { it('should inject custom opts to opts and without callback', () => { - scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }); + scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }) - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); - }); + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') + }) it('should inject custom opts to opts and with callback', () => { scenarioConfig = context.Scenario.skip('scenario', { customOpts: 'Custom Opts' }, () => { - console.log('Scenario Body'); - }); - - expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts'); - }); - }); - }); -}); + console.log('Scenario Body') + }) + + expect(scenarioConfig.test.opts.customOpts).eq('Custom Opts') + }) + }) + }) +}) From 371698e075115cc547f4ba3dd8d2a351c19b64af Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sat, 4 Jan 2025 04:32:37 +0200 Subject: [PATCH 06/13] workers fix --- lib/workers.js | 476 ++++++++++++++++++++++++------------------------- 1 file changed, 236 insertions(+), 240 deletions(-) diff --git a/lib/workers.js b/lib/workers.js index c35b94677..3618f9c35 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -1,56 +1,56 @@ -const path = require('path'); -const mkdirp = require('mkdirp'); -const { Worker } = require('worker_threads'); +const path = require('path') +const mkdirp = require('mkdirp') +const { Worker } = require('worker_threads') const { Suite, Test, reporters: { Base }, -} = require('mocha'); -const { EventEmitter } = require('events'); -const ms = require('ms'); -const Codecept = require('./codecept'); -const MochaFactory = require('./mocha/factory'); -const Container = require('./container'); -const { getTestRoot } = require('./command/utils'); -const { isFunction, fileExists } = require('./utils'); -const { replaceValueDeep, deepClone } = require('./utils'); -const mainConfig = require('./config'); -const output = require('./output'); -const event = require('./event'); -const recorder = require('./recorder'); -const runHook = require('./hooks'); -const WorkerStorage = require('./workerStorage'); -const collection = require('./command/run-multiple/collection'); - -const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js'); +} = require('mocha') +const { EventEmitter } = require('events') +const ms = require('ms') +const Codecept = require('./codecept') +const MochaFactory = require('./mocha/factory') +const Container = require('./container') +const { getTestRoot } = require('./command/utils') +const { isFunction, fileExists } = require('./utils') +const { replaceValueDeep, deepClone } = require('./utils') +const mainConfig = require('./config') +const output = require('./output') +const event = require('./event') +const recorder = require('./recorder') +const runHook = require('./hooks') +const WorkerStorage = require('./workerStorage') +const collection = require('./command/run-multiple/collection') + +const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js') const initializeCodecept = (configPath, options = {}) => { - const codecept = new Codecept(mainConfig.load(configPath || '.'), options); - codecept.init(getTestRoot(configPath)); - codecept.loadTests(); + const codecept = new Codecept(mainConfig.load(configPath || '.'), options) + codecept.init(getTestRoot(configPath)) + codecept.loadTests() - return codecept; -}; + return codecept +} const createOutputDir = configPath => { - const config = mainConfig.load(configPath || '.'); - const testRoot = getTestRoot(configPath); - const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output); + const config = mainConfig.load(configPath || '.') + const testRoot = getTestRoot(configPath) + const outputDir = path.isAbsolute(config.output) ? config.output : path.join(testRoot, config.output) if (!fileExists(outputDir)) { - output.print(`creating output directory: ${outputDir}`); - mkdirp.sync(outputDir); + output.print(`creating output directory: ${outputDir}`) + mkdirp.sync(outputDir) } -}; +} const populateGroups = numberOfWorkers => { - const groups = []; + const groups = [] for (let i = 0; i < numberOfWorkers; i++) { - groups[i] = []; + groups[i] = [] } - return groups; -}; + return groups +} const createWorker = workerObject => { const worker = new Worker(pathToWorker, { @@ -60,12 +60,12 @@ const createWorker = workerObject => { testRoot: workerObject.testRoot, workerIndex: workerObject.workerIndex + 1, }, - }); - worker.on('error', err => output.error(`Worker Error: ${err.stack}`)); + }) + worker.on('error', err => output.error(`Worker Error: ${err.stack}`)) - WorkerStorage.addWorker(worker); - return worker; -}; + WorkerStorage.addWorker(worker) + return worker +} const simplifyObject = object => { return Object.keys(object) @@ -73,161 +73,161 @@ const simplifyObject = object => { .filter(k => typeof object[k] !== 'function') .filter(k => typeof object[k] !== 'object') .reduce((obj, key) => { - obj[key] = object[key]; - return obj; - }, {}); -}; + obj[key] = object[key] + return obj + }, {}) +} const repackTest = test => { - test = Object.assign(new Test(test.title || '', () => {}), test); - test.parent = Object.assign(new Suite(test.parent.title), test.parent); - return test; -}; + test = Object.assign(new Test(test.title || '', () => {}), test) + test.parent = Object.assign(new Suite(test.parent.title), test.parent) + return test +} const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns) => { - selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns; + selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns if (selectedRuns === undefined || !selectedRuns.length || config.multiple === undefined) { return testGroups.map((tests, index) => { - const workerObj = new WorkerObject(index); - workerObj.addConfig(config); - workerObj.addTests(tests); - workerObj.setTestRoot(testRoot); - workerObj.addOptions(options); - return workerObj; - }); + const workerObj = new WorkerObject(index) + workerObj.addConfig(config) + workerObj.addTests(tests) + workerObj.setTestRoot(testRoot) + workerObj.addOptions(options) + return workerObj + }) } - const workersToExecute = []; + const workersToExecute = [] - const currentOutputFolder = config.output; - let currentMochawesomeReportDir; - let currentMochaJunitReporterFile; + const currentOutputFolder = config.output + let currentMochawesomeReportDir + let currentMochaJunitReporterFile if (config.mocha && config.mocha.reporterOptions) { - currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir; - currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile; + currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir + currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile } collection.createRuns(selectedRuns, config).forEach(worker => { - const separator = path.sep; - const _config = { ...config }; - let workerName = worker.name.replace(':', '_'); - _config.output = `${currentOutputFolder}${separator}${workerName}`; + const separator = path.sep + const _config = { ...config } + let workerName = worker.name.replace(':', '_') + _config.output = `${currentOutputFolder}${separator}${workerName}` if (config.mocha && config.mocha.reporterOptions) { - _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}`; + _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}` - const _tempArray = currentMochaJunitReporterFile.split(separator); + const _tempArray = currentMochaJunitReporterFile.split(separator) _tempArray.splice( _tempArray.findIndex(item => item.includes('.xml')), 0, workerName, - ); - _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator); + ) + _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator) } - workerName = worker.getOriginalName() || worker.getName(); - const workerConfig = worker.getConfig(); - workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config)); - }); - const workers = []; - let index = 0; + workerName = worker.getOriginalName() || worker.getName() + const workerConfig = worker.getConfig() + workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config)) + }) + const workers = [] + let index = 0 testGroups.forEach(tests => { - const testWorkerArray = []; + const testWorkerArray = [] workersToExecute.forEach(finalConfig => { - const workerObj = new WorkerObject(index++); - workerObj.addConfig(finalConfig); - workerObj.addTests(tests); - workerObj.setTestRoot(testRoot); - workerObj.addOptions(options); - testWorkerArray.push(workerObj); - }); - workers.push(...testWorkerArray); - }); - return workers; -}; + const workerObj = new WorkerObject(index++) + workerObj.addConfig(finalConfig) + workerObj.addTests(tests) + workerObj.setTestRoot(testRoot) + workerObj.addOptions(options) + testWorkerArray.push(workerObj) + }) + workers.push(...testWorkerArray) + }) + return workers +} const indexOfSmallestElement = groups => { - let i = 0; + let i = 0 for (let j = 1; j < groups.length; j++) { if (groups[j - 1].length > groups[j].length) { - i = j; + i = j } } - return i; -}; + return i +} const convertToMochaTests = testGroup => { - const group = []; + const group = [] if (testGroup instanceof Array) { - const mocha = MochaFactory.create({}, {}); - mocha.files = testGroup; - mocha.loadFiles(); + const mocha = MochaFactory.create({}, {}) + mocha.files = testGroup + mocha.loadFiles() mocha.suite.eachTest(test => { - group.push(test.uid); - }); - mocha.unloadFiles(); + group.push(test.uid) + }) + mocha.unloadFiles() } - return group; -}; + return group +} const getOverridenConfig = (workerName, workerConfig, config) => { // clone config - const overriddenConfig = deepClone(config); + const overriddenConfig = deepClone(config) // get configuration - const browserConfig = workerConfig.browser; + const browserConfig = workerConfig.browser for (const key in browserConfig) { - overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]); + overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]) } // override tests configuration if (overriddenConfig.tests) { - overriddenConfig.tests = workerConfig.tests; + overriddenConfig.tests = workerConfig.tests } if (overriddenConfig.gherkin && workerConfig.gherkin && workerConfig.gherkin.features) { - overriddenConfig.gherkin.features = workerConfig.gherkin.features; + overriddenConfig.gherkin.features = workerConfig.gherkin.features } - return overriddenConfig; -}; + return overriddenConfig +} class WorkerObject { /** * @param {Number} workerIndex - Unique ID for worker */ constructor(workerIndex) { - this.workerIndex = workerIndex; - this.options = {}; - this.tests = []; - this.testRoot = getTestRoot(); + this.workerIndex = workerIndex + this.options = {} + this.tests = [] + this.testRoot = getTestRoot() } addConfig(config) { - const oldConfig = JSON.parse(this.options.override || '{}'); + const oldConfig = JSON.parse(this.options.override || '{}') const newConfig = { ...oldConfig, ...config, - }; - this.options.override = JSON.stringify(newConfig); + } + this.options.override = JSON.stringify(newConfig) } addTestFiles(testGroup) { - this.addTests(convertToMochaTests(testGroup)); + this.addTests(convertToMochaTests(testGroup)) } addTests(tests) { - this.tests = this.tests.concat(tests); + this.tests = this.tests.concat(tests) } setTestRoot(path) { - this.testRoot = getTestRoot(path); + this.testRoot = getTestRoot(path) } addOptions(opts) { this.options = { ...this.options, ...opts, - }; + } } } @@ -237,30 +237,30 @@ class Workers extends EventEmitter { * @param {Object} config */ constructor(numberOfWorkers, config = { by: 'test' }) { - super(); - this.setMaxListeners(50); - this.codecept = initializeCodecept(config.testConfig, config.options); - this.failuresLog = []; - this.errors = []; - this.numberOfWorkers = 0; - this.closedWorkers = 0; - this.workers = []; + super() + this.setMaxListeners(50) + this.codecept = initializeCodecept(config.testConfig, config.options) + this.failuresLog = [] + this.errors = [] + this.numberOfWorkers = 0 + this.closedWorkers = 0 + this.workers = [] this.stats = { passes: 0, failures: 0, tests: 0, pending: 0, - }; - this.testGroups = []; + } + this.testGroups = [] - createOutputDir(config.testConfig); - if (numberOfWorkers) this._initWorkers(numberOfWorkers, config); + createOutputDir(config.testConfig) + if (numberOfWorkers) this._initWorkers(numberOfWorkers, config) } _initWorkers(numberOfWorkers, config) { - this.splitTestsByGroups(numberOfWorkers, config); - this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns); - this.numberOfWorkers = this.workers.length; + this.splitTestsByGroups(numberOfWorkers, config) + this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns) + this.numberOfWorkers = this.workers.length } /** @@ -277,16 +277,16 @@ class Workers extends EventEmitter { */ splitTestsByGroups(numberOfWorkers, config) { if (isFunction(config.by)) { - const createTests = config.by; - const testGroups = createTests(numberOfWorkers); + const createTests = config.by + const testGroups = createTests(numberOfWorkers) if (!(testGroups instanceof Array)) { - throw new Error('Test group should be an array'); + throw new Error('Test group should be an array') } for (const testGroup of testGroups) { - this.testGroups.push(convertToMochaTests(testGroup)); + this.testGroups.push(convertToMochaTests(testGroup)) } } else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) { - this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers); + this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers) } } @@ -296,53 +296,53 @@ class Workers extends EventEmitter { * @returns {WorkerObject} */ spawn() { - const worker = new WorkerObject(this.numberOfWorkers); - this.workers.push(worker); - this.numberOfWorkers += 1; - return worker; + const worker = new WorkerObject(this.numberOfWorkers) + this.workers.push(worker) + this.numberOfWorkers += 1 + return worker } /** * @param {Number} numberOfWorkers */ createGroupsOfTests(numberOfWorkers) { - const files = this.codecept.testFiles; - const mocha = Container.mocha(); - mocha.files = files; - mocha.loadFiles(); + const files = this.codecept.testFiles + const mocha = Container.mocha() + mocha.files = files + mocha.loadFiles() - const groups = populateGroups(numberOfWorkers); - let groupCounter = 0; + const groups = populateGroups(numberOfWorkers) + let groupCounter = 0 mocha.suite.eachTest(test => { - const i = groupCounter % groups.length; + const i = groupCounter % groups.length if (test) { - groups[i].push(test.uid); - groupCounter++; + groups[i].push(test.uid) + groupCounter++ } - }); - return groups; + }) + return groups } /** * @param {Number} numberOfWorkers */ createGroupsOfSuites(numberOfWorkers) { - const files = this.codecept.testFiles; - const groups = populateGroups(numberOfWorkers); + const files = this.codecept.testFiles + const groups = populateGroups(numberOfWorkers) - const mocha = Container.mocha(); - mocha.files = files; - mocha.loadFiles(); + const mocha = Container.mocha() + mocha.files = files + mocha.loadFiles() mocha.suite.suites.forEach(suite => { - const i = indexOfSmallestElement(groups); + const i = indexOfSmallestElement(groups) suite.tests.forEach(test => { if (test) { - groups[i].push(test.uid); + groups[i].push(test.uid) } - }); - }); - return groups; + }) + }) + return groups } /** @@ -350,160 +350,156 @@ class Workers extends EventEmitter { */ overrideConfig(config) { for (const worker of this.workers) { - worker.addConfig(config); + worker.addConfig(config) } } async bootstrapAll() { - return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll'); + return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll') } async teardownAll() { - return runHook(this.codecept.config.teardownAll, 'teardownAll'); + return runHook(this.codecept.config.teardownAll, 'teardownAll') } run() { - this.stats.start = new Date(); - this.stats.failedHooks = 0; - recorder.startUnlessRunning(); - event.dispatcher.emit(event.workers.before); - process.env.RUNS_WITH_WORKERS = 'true'; + this.stats.start = new Date() + this.stats.failedHooks = 0 + recorder.startUnlessRunning() + event.dispatcher.emit(event.workers.before) + process.env.RUNS_WITH_WORKERS = 'true' recorder.add('starting workers', () => { for (const worker of this.workers) { - const workerThread = createWorker(worker); - this._listenWorkerEvents(workerThread); + const workerThread = createWorker(worker) + this._listenWorkerEvents(workerThread) } - }); + }) return new Promise(resolve => { - this.on('end', resolve); - }); + this.on('end', resolve) + }) } /** * @returns {Array} */ getWorkers() { - return this.workers; + return this.workers } /** * @returns {Boolean} */ isFailed() { - return (this.stats.failures || this.errors.length) > 0; + return (this.stats.failures || this.errors.length) > 0 } _listenWorkerEvents(worker) { worker.on('message', message => { - output.process(message.workerIndex); + output.process(message.workerIndex) // deal with events that are not test cycle related if (!message.event) { - return this.emit('message', message); + return this.emit('message', message) } switch (message.event) { case event.all.failures: - this.failuresLog = this.failuresLog.concat(message.data.failuresLog); - this._appendStats(message.data.stats); - break; + this.failuresLog = this.failuresLog.concat(message.data.failuresLog) + this._appendStats(message.data.stats) + break case event.suite.before: - this.emit(event.suite.before, repackTest(message.data)); - break; - case event.hook.failed: - this.emit(event.hook.failed, repackTest(message.data)); - this.errors.push(message.data.err); - break; + this.emit(event.suite.before, repackTest(message.data)) + break case event.test.before: - this.emit(event.test.before, repackTest(message.data)); - break; + this.emit(event.test.before, repackTest(message.data)) + break case event.test.started: - this.emit(event.test.started, repackTest(message.data)); - break; + this.emit(event.test.started, repackTest(message.data)) + break case event.test.failed: - this.emit(event.test.failed, repackTest(message.data)); - break; + this.emit(event.test.failed, repackTest(message.data)) + break case event.test.passed: - this.emit(event.test.passed, repackTest(message.data)); - break; + this.emit(event.test.passed, repackTest(message.data)) + break case event.test.skipped: - this.emit(event.test.skipped, repackTest(message.data)); - break; + this.emit(event.test.skipped, repackTest(message.data)) + break case event.test.finished: - this.emit(event.test.finished, repackTest(message.data)); - break; + this.emit(event.test.finished, repackTest(message.data)) + break case event.test.after: - this.emit(event.test.after, repackTest(message.data)); - break; + this.emit(event.test.after, repackTest(message.data)) + break case event.step.finished: - this.emit(event.step.finished, message.data); - break; + this.emit(event.step.finished, message.data) + break case event.step.started: - this.emit(event.step.started, message.data); - break; + this.emit(event.step.started, message.data) + break case event.step.passed: - this.emit(event.step.passed, message.data); - break; + this.emit(event.step.passed, message.data) + break case event.step.failed: - this.emit(event.step.failed, message.data); - break; + this.emit(event.step.failed, message.data) + break } - }); + }) worker.on('error', err => { - this.errors.push(err); - }); + this.errors.push(err) + }) worker.on('exit', () => { - this.closedWorkers += 1; + this.closedWorkers += 1 if (this.closedWorkers === this.numberOfWorkers) { - this._finishRun(); + this._finishRun() } - }); + }) } _finishRun() { - event.dispatcher.emit(event.workers.after); + event.dispatcher.emit(event.workers.after) if (this.isFailed()) { - process.exitCode = 1; + process.exitCode = 1 } else { - process.exitCode = 0; + process.exitCode = 0 } // removed this.finishedTests because in all /lib only first argument (!this.isFailed()) is used) - this.emit(event.all.result, !this.isFailed()); - this.emit('end'); // internal event + this.emit(event.all.result, !this.isFailed()) + this.emit('end') // internal event } _appendStats(newStats) { - this.stats.passes += newStats.passes; - this.stats.failures += newStats.failures; - this.stats.tests += newStats.tests; - this.stats.pending += newStats.pending; - this.stats.failedHooks += newStats.failedHooks; + this.stats.passes += newStats.passes + this.stats.failures += newStats.failures + this.stats.tests += newStats.tests + this.stats.pending += newStats.pending + this.stats.failedHooks += newStats.failedHooks } printResults() { - this.stats.end = new Date(); - this.stats.duration = this.stats.end - this.stats.start; + this.stats.end = new Date() + this.stats.duration = this.stats.end - this.stats.start // Reset process for logs in main thread - output.process(null); - output.print(); + output.process(null) + output.print() this.failuresLog = this.failuresLog .filter(log => log.length && typeof log[1] === 'number') // mocha/lib/reporters/base.js - .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack]); + .map(([format, num, title, message, stack], i) => [format, i + 1, title, message, stack]) if (this.failuresLog.length) { - output.print(); - output.print('-- FAILURES:'); - this.failuresLog.forEach(log => output.print(...log)); + output.print() + output.print('-- FAILURES:') + this.failuresLog.forEach(log => output.print(...log)) } - output.result(this.stats.passes, this.stats.failures, this.stats.pending, ms(this.stats.duration), this.stats.failedHooks); - process.env.RUNS_WITH_WORKERS = 'false'; + output.result(this.stats.passes, this.stats.failures, this.stats.pending, ms(this.stats.duration), this.stats.failedHooks) + process.env.RUNS_WITH_WORKERS = 'false' } } -module.exports = Workers; +module.exports = Workers From fd497a994db5c5351a0ab1bceeb24f2d9465e9f8 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sat, 4 Jan 2025 04:38:27 +0200 Subject: [PATCH 07/13] fixed def --- docs/plugins.md | 12 ++++++------ typings/jsdoc.conf.js | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 30e3b5e69..155550f7d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -285,7 +285,7 @@ Scenario('login', async ({ I, login }) => { Add descriptive nested steps for your tests: ```js -Scenario('project update test', async (I) => { +Scenario('project update test', async I => { __`Given` const projectId = await I.have('project') @@ -559,7 +559,7 @@ Example of usage: ```js // this example works with Playwright and Puppeteer helper -await eachElement('click all checkboxes', 'form input[type=checkbox]', async (el) => { +await eachElement('click all checkboxes', 'form input[type=checkbox]', async el => { await el.click() }) ``` @@ -578,7 +578,7 @@ Check all elements for visibility: ```js // this example works with Playwright and Puppeteer helper const assert = require('assert') -await eachElement('check all items are visible', '.item', async (el) => { +await eachElement('check all items are visible', '.item', async el => { assert(await el.isVisible()) }) ``` @@ -750,7 +750,7 @@ Use scenario configuration to disable plugin for a test ```js Scenario('scenario tite', () => { // test goes here -}).config((test) => (test.disableRetryFailedStep = true)) +}).config(test => (test.disableRetryFailedStep = true)) ``` ### Parameters @@ -775,7 +775,7 @@ Use it in your tests: ```js // retry these steps 5 times before failing -await retryTo((tryNum) => { +await retryTo(tryNum => { I.switchTo('#editor frame') I.click('Open') I.see('Opened') @@ -787,7 +787,7 @@ Set polling interval as 3rd argument (200ms by default): ```js // retry these steps 5 times before failing await retryTo( - (tryNum) => { + tryNum => { I.switchTo('#editor frame') I.click('Open') I.see('Opened') diff --git a/typings/jsdoc.conf.js b/typings/jsdoc.conf.js index f14ec41a4..3ff9b4c04 100644 --- a/typings/jsdoc.conf.js +++ b/typings/jsdoc.conf.js @@ -10,7 +10,6 @@ module.exports = { './lib/data/dataTableArgument.js', './lib/event.js', './lib/index.js', - './lib/interfaces', './lib/locator.js', './lib/output.js', './lib/pause.js', @@ -19,7 +18,11 @@ module.exports = { './lib/session.js', './lib/step.js', './lib/store.js', - './lib/ui.js', + './lib/mocha/ui.js', + './lib/mocha/featureConfig.js', + './lib/mocha/scenarioConfig.js', + './lib/mocha/bdd.js', + './lib/mocha/hooks.js', './lib/within.js', require.resolve('@codeceptjs/detox-helper'), require.resolve('@codeceptjs/helper'), @@ -31,4 +34,4 @@ module.exports = { destination: './typings/', }, plugins: ['jsdoc.namespace.js', 'jsdoc-typeof-plugin'], -}; +} From 40434aa02ab124ac030890f33b3e2504c127c50a Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sat, 4 Jan 2025 23:09:54 +0200 Subject: [PATCH 08/13] fixed type definitions --- lib/listener/steps.js | 108 ++++++++++++++++++------------------ lib/mocha/featureConfig.js | 4 ++ lib/mocha/scenarioConfig.js | 10 +++- 3 files changed, 67 insertions(+), 55 deletions(-) diff --git a/lib/listener/steps.js b/lib/listener/steps.js index 106744288..38d166c0b 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -1,88 +1,88 @@ -const debug = require('debug')('codeceptjs:steps'); -const event = require('../event'); -const store = require('../store'); -const output = require('../output'); -const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks'); +const debug = require('debug')('codeceptjs:steps') +const event = require('../event') +const store = require('../store') +const output = require('../output') +const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks') -let currentTest; -let currentHook; +let currentTest +let currentHook /** * Register steps inside tests */ module.exports = function () { event.dispatcher.on(event.test.before, test => { - test.startedAt = +new Date(); - test.artifacts = {}; - }); + test.startedAt = +new Date() + test.artifacts = {} + }) event.dispatcher.on(event.test.started, test => { - currentTest = test; - currentTest.steps = []; - if (!('retryNum' in currentTest)) currentTest.retryNum = 0; - else currentTest.retryNum += 1; - output.scenario.started(test); - }); + currentTest = test + currentTest.steps = [] + if (!('retryNum' in currentTest)) currentTest.retryNum = 0 + else currentTest.retryNum += 1 + output.scenario.started(test) + }) event.dispatcher.on(event.test.after, test => { - currentTest = null; - }); + currentTest = null + }) - event.dispatcher.on(event.test.finished, test => {}); + event.dispatcher.on(event.test.finished, test => {}) event.dispatcher.on(event.hook.started, hook => { - currentHook = hook.ctx.test; - currentHook.steps = []; + currentHook = hook.ctx.test + currentHook.steps = [] - output.hook.started(hook); + output.hook.started(hook) - if (hook.ctx && hook.ctx.test) output.log(`--- STARTED ${hook.ctx.test.title} ---`); - }); + if (hook.ctx && hook.ctx.test) debug(`--- STARTED ${hook.ctx.test.title} ---`) + }) event.dispatcher.on(event.hook.passed, hook => { - currentHook = null; - output.hook.passed(hook); - if (hook.ctx && hook.ctx.test) output.log(`--- ENDED ${hook.ctx.test.title} ---`); - }); + currentHook = null + output.hook.passed(hook) + if (hook.ctx && hook.ctx.test) debug(`--- ENDED ${hook.ctx.test.title} ---`) + }) event.dispatcher.on(event.test.failed, () => { const cutSteps = function (current) { - const failureIndex = current.steps.findIndex(el => el.status === 'failed'); + const failureIndex = current.steps.findIndex(el => el.status === 'failed') // To be sure that failed test will be failed in report - current.state = 'failed'; - current.steps.length = failureIndex + 1; - return current; - }; + current.state = 'failed' + current.steps.length = failureIndex + 1 + return current + } if (currentHook && Array.isArray(currentHook.steps) && currentHook.steps.length) { - currentHook = cutSteps(currentHook); - return (currentHook = null); + currentHook = cutSteps(currentHook) + return (currentHook = null) } - if (!currentTest) return; + if (!currentTest) return // last step is failing step - if (!currentTest.steps.length) return; - return (currentTest = cutSteps(currentTest)); - }); + if (!currentTest.steps.length) return + return (currentTest = cutSteps(currentTest)) + }) event.dispatcher.on(event.test.passed, () => { - if (!currentTest) return; + if (!currentTest) return // To be sure that passed test will be passed in report - delete currentTest.err; - currentTest.state = 'passed'; - }); + delete currentTest.err + currentTest.state = 'passed' + }) event.dispatcher.on(event.step.started, step => { - step.startedAt = +new Date(); - step.test = currentTest; + step.startedAt = +new Date() + step.test = currentTest if (currentHook && Array.isArray(currentHook.steps)) { - return currentHook.steps.push(step); + return currentHook.steps.push(step) } - if (!currentTest || !currentTest.steps) return; - currentTest.steps.push(step); - }); + if (!currentTest || !currentTest.steps) return + currentTest.steps.push(step) + }) event.dispatcher.on(event.step.finished, step => { - step.finishedAt = +new Date(); - if (step.startedAt) step.duration = step.finishedAt - step.startedAt; - debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`); - }); -}; + step.finishedAt = +new Date() + if (step.startedAt) step.duration = step.finishedAt - step.startedAt + debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`) + }) +} diff --git a/lib/mocha/featureConfig.js b/lib/mocha/featureConfig.js index 7c3531a99..0948cb60a 100644 --- a/lib/mocha/featureConfig.js +++ b/lib/mocha/featureConfig.js @@ -31,6 +31,10 @@ class FeatureConfig { /** * Configures a helper. * Helper name can be omitted and values will be applied to first helper. + * + * @param {string|number} helper + * @param {*} obj + * @returns {this} */ config(helper, obj) { if (!obj) { diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index f5d82ed0d..3c559b12b 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -12,6 +12,7 @@ class ScenarioConfig { * Can pass an Error object or regex matching expected message. * * @param {*} err + * @returns {this} */ throws(err) { this.test.throws = err @@ -23,7 +24,7 @@ class ScenarioConfig { * If test passes - throws an error. * Can pass an Error object or regex matching expected message. * - * @param {*} err + * @returns {this} */ fails() { this.test.throws = new Error() @@ -34,6 +35,7 @@ class ScenarioConfig { * Retry this test for number of times * * @param {number} retries + * @returns {this} */ retry(retries) { this.test.retries(retries) @@ -43,6 +45,7 @@ class ScenarioConfig { /** * Set timeout for this test * @param {number} timeout + * @returns {this} */ timeout(timeout) { this.test.timeout(timeout) @@ -52,6 +55,7 @@ class ScenarioConfig { /** * Pass in additional objects to inject into test * @param {*} obj + * @returns {this} */ inject(obj) { this.test.inject = obj @@ -61,6 +65,9 @@ class ScenarioConfig { /** * Configures a helper. * Helper name can be omitted and values will be applied to first helper. + * @param {string|number} helper + * @param {*} obj + * @returns {this} */ config(helper, obj) { if (!obj) { @@ -80,6 +87,7 @@ class ScenarioConfig { /** * Append a tag name to scenario title * @param {string} tagName + * @returns {this} */ tag(tagName) { if (tagName[0] !== '@') tagName = `@${tagName}` From 01bfdb2d0f13cb5d6e36c0d30d5cfa77116d236b Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 5 Jan 2025 04:30:34 +0200 Subject: [PATCH 09/13] refactored mocha classes --- lib/codecept.js | 165 +++++++++--------- lib/listener/{retry.js => globalRetry.js} | 0 lib/listener/{timeout.js => globalTimeout.js} | 0 lib/listener/store.js | 12 ++ lib/mocha/featureConfig.js | 1 + lib/mocha/index.js | 12 ++ lib/mocha/scenario.js | 1 - lib/mocha/scenarioConfig.js | 28 +-- lib/mocha/suite.js | 55 ++++++ lib/mocha/test.js | 58 ++++++ lib/mocha/types.d.ts | 30 ++++ lib/mocha/ui.js | 38 +--- lib/store.js | 7 +- .../data/sandbox/configs/bootstrap/fs_test.js | 24 +-- test/runner/bootstrap_test.js | 2 + test/runner/codecept_test.js | 2 + test/runner/session_test.js | 4 +- test/runner/within_test.js | 4 +- test/support/TestHelper.js | 6 + test/unit/ui_test.js | 2 + typings/index.d.ts | 5 +- 21 files changed, 314 insertions(+), 142 deletions(-) rename lib/listener/{retry.js => globalRetry.js} (100%) rename lib/listener/{timeout.js => globalTimeout.js} (100%) create mode 100644 lib/listener/store.js create mode 100644 lib/mocha/index.js create mode 100644 lib/mocha/suite.js create mode 100644 lib/mocha/test.js create mode 100644 lib/mocha/types.d.ts diff --git a/lib/codecept.js b/lib/codecept.js index 2a6d00e10..b924425ee 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -1,14 +1,14 @@ -const { existsSync, readFileSync } = require('fs'); -const glob = require('glob'); -const fsPath = require('path'); -const { resolve } = require('path'); - -const container = require('./container'); -const Config = require('./config'); -const event = require('./event'); -const runHook = require('./hooks'); -const output = require('./output'); -const { emptyFolder } = require('./utils'); +const { existsSync, readFileSync } = require('fs') +const glob = require('glob') +const fsPath = require('path') +const { resolve } = require('path') + +const container = require('./container') +const Config = require('./config') +const event = require('./event') +const runHook = require('./hooks') +const output = require('./output') +const { emptyFolder } = require('./utils') /** * CodeceptJS runner @@ -22,10 +22,10 @@ class Codecept { * @param {*} opts */ constructor(config, opts) { - this.config = Config.create(config); - this.opts = opts; - this.testFiles = new Array(0); - this.requireModules(config.require); + this.config = Config.create(config) + this.opts = opts + this.testFiles = new Array(0) + this.requireModules(config.require) } /** @@ -36,12 +36,12 @@ class Codecept { requireModules(requiringModules) { if (requiringModules) { requiringModules.forEach(requiredModule => { - const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`); + const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`) if (isLocalFile) { - requiredModule = resolve(requiredModule); + requiredModule = resolve(requiredModule) } - require(requiredModule); - }); + require(requiredModule) + }) } } @@ -52,10 +52,10 @@ class Codecept { * @param {string} dir */ init(dir) { - this.initGlobals(dir); + this.initGlobals(dir) // initializing listeners - container.create(this.config, this.opts); - this.runHooks(); + container.create(this.config, this.opts) + this.runHooks() } /** @@ -64,37 +64,37 @@ class Codecept { * @param {string} dir */ initGlobals(dir) { - global.codecept_dir = dir; - global.output_dir = fsPath.resolve(dir, this.config.output); + global.codecept_dir = dir + global.output_dir = fsPath.resolve(dir, this.config.output) - if (this.config.emptyOutputFolder) emptyFolder(global.output_dir); + if (this.config.emptyOutputFolder) emptyFolder(global.output_dir) if (!this.config.noGlobals) { - global.Helper = global.codecept_helper = require('@codeceptjs/helper'); - global.actor = global.codecept_actor = require('./actor'); - global.pause = require('./pause'); - global.within = require('./within'); - global.session = require('./session'); - global.DataTable = require('./data/table'); - global.locate = locator => require('./locator').build(locator); - global.inject = container.support; - global.share = container.share; - global.secret = require('./secret').secret; - global.codecept_debug = output.debug; - global.codeceptjs = require('./index'); // load all objects + global.Helper = global.codecept_helper = require('@codeceptjs/helper') + global.actor = global.codecept_actor = require('./actor') + global.pause = require('./pause') + global.within = require('./within') + global.session = require('./session') + global.DataTable = require('./data/table') + global.locate = locator => require('./locator').build(locator) + global.inject = container.support + global.share = container.share + global.secret = require('./secret').secret + global.codecept_debug = output.debug + global.codeceptjs = require('./index') // load all objects // BDD - const stepDefinitions = require('./mocha/bdd'); - global.Given = stepDefinitions.Given; - global.When = stepDefinitions.When; - global.Then = stepDefinitions.Then; - global.DefineParameterType = stepDefinitions.defineParameterType; + const stepDefinitions = require('./mocha/bdd') + global.Given = stepDefinitions.Given + global.When = stepDefinitions.When + global.Then = stepDefinitions.Then + global.DefineParameterType = stepDefinitions.defineParameterType // debug mode - global.debugMode = false; + global.debugMode = false // mask sensitive data - global.maskSensitiveData = this.config.maskSensitiveData || false; + global.maskSensitiveData = this.config.maskSensitiveData || false } } @@ -103,16 +103,17 @@ class Codecept { */ runHooks() { // default hooks - runHook(require('./listener/steps')); - runHook(require('./listener/artifacts')); - runHook(require('./listener/config')); - runHook(require('./listener/helpers')); - runHook(require('./listener/retry')); - runHook(require('./listener/timeout')); - runHook(require('./listener/exit')); + runHook(require('./listener/store')) + runHook(require('./listener/steps')) + runHook(require('./listener/artifacts')) + runHook(require('./listener/config')) + runHook(require('./listener/helpers')) + runHook(require('./listener/globalTimeout')) + runHook(require('./listener/globalRetry')) + runHook(require('./listener/exit')) // custom hooks (previous iteration of plugins) - this.config.hooks.forEach(hook => runHook(hook)); + this.config.hooks.forEach(hook => runHook(hook)) } /** @@ -120,7 +121,7 @@ class Codecept { * */ async bootstrap() { - return runHook(this.config.bootstrap, 'bootstrap'); + return runHook(this.config.bootstrap, 'bootstrap') } /** @@ -128,7 +129,7 @@ class Codecept { */ async teardown() { - return runHook(this.config.teardown, 'teardown'); + return runHook(this.config.teardown, 'teardown') } /** @@ -139,42 +140,42 @@ class Codecept { loadTests(pattern) { const options = { cwd: global.codecept_dir, - }; + } - let patterns = [pattern]; + let patterns = [pattern] if (!pattern) { - patterns = []; + patterns = [] // If the user wants to test a specific set of test files as an array or string. if (this.config.tests && !this.opts.features) { if (Array.isArray(this.config.tests)) { - patterns.push(...this.config.tests); + patterns.push(...this.config.tests) } else { - patterns.push(this.config.tests); + patterns.push(this.config.tests) } } if (this.config.gherkin.features && !this.opts.tests) { if (Array.isArray(this.config.gherkin.features)) { this.config.gherkin.features.forEach(feature => { - patterns.push(feature); - }); + patterns.push(feature) + }) } else { - patterns.push(this.config.gherkin.features); + patterns.push(this.config.gherkin.features) } } } for (pattern of patterns) { glob.sync(pattern, options).forEach(file => { - if (file.includes('node_modules')) return; + if (file.includes('node_modules')) return if (!fsPath.isAbsolute(file)) { - file = fsPath.join(global.codecept_dir, file); + file = fsPath.join(global.codecept_dir, file) } if (!this.testFiles.includes(fsPath.resolve(file))) { - this.testFiles.push(fsPath.resolve(file)); + this.testFiles.push(fsPath.resolve(file)) } - }); + }) } } @@ -185,36 +186,36 @@ class Codecept { * @returns {Promise} */ async run(test) { - await container.started(); + await container.started() return new Promise((resolve, reject) => { - const mocha = container.mocha(); - mocha.files = this.testFiles; + const mocha = container.mocha() + mocha.files = this.testFiles if (test) { if (!fsPath.isAbsolute(test)) { - test = fsPath.join(global.codecept_dir, test); + test = fsPath.join(global.codecept_dir, test) } - mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test); + mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test) } const done = () => { - event.emit(event.all.result, this); - event.emit(event.all.after, this); - resolve(); - }; + event.emit(event.all.result, this) + event.emit(event.all.after, this) + resolve() + } try { - event.emit(event.all.before, this); - mocha.run(() => done()); + event.emit(event.all.before, this) + mocha.run(() => done()) } catch (e) { - output.error(e.stack); - reject(e); + output.error(e.stack) + reject(e) } - }); + }) } static version() { - return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version; + return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version } } -module.exports = Codecept; +module.exports = Codecept diff --git a/lib/listener/retry.js b/lib/listener/globalRetry.js similarity index 100% rename from lib/listener/retry.js rename to lib/listener/globalRetry.js diff --git a/lib/listener/timeout.js b/lib/listener/globalTimeout.js similarity index 100% rename from lib/listener/timeout.js rename to lib/listener/globalTimeout.js diff --git a/lib/listener/store.js b/lib/listener/store.js new file mode 100644 index 000000000..763aa1edc --- /dev/null +++ b/lib/listener/store.js @@ -0,0 +1,12 @@ +const event = require('../event') +const store = require('../store') + +module.exports = function () { + event.dispatcher.on(event.test.before, test => { + store.currentTest = test + }) + + event.dispatcher.on(event.test.finished, test => { + store.currentTest = null + }) +} diff --git a/lib/mocha/featureConfig.js b/lib/mocha/featureConfig.js index 0948cb60a..a80ab1dfe 100644 --- a/lib/mocha/featureConfig.js +++ b/lib/mocha/featureConfig.js @@ -60,6 +60,7 @@ class FeatureConfig { if (tagName[0] !== '@') tagName = `@${tagName}` if (!this.suite.tags) this.suite.tags = [] this.suite.tags.push(tagName) + this.suite.title = `${this.suite.title.trim()} ${tagName}` return this } } diff --git a/lib/mocha/index.js b/lib/mocha/index.js new file mode 100644 index 000000000..b343fbff9 --- /dev/null +++ b/lib/mocha/index.js @@ -0,0 +1,12 @@ +const Suite = require('mocha/lib/suite') +const Test = require('mocha/lib/test') +const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('./hooks') + +module.exports = { + Suite, + Test, + BeforeHook, + AfterHook, + BeforeSuiteHook, + AfterSuiteHook, +} diff --git a/lib/mocha/scenario.js b/lib/mocha/scenario.js index 48b503263..8609bea3b 100644 --- a/lib/mocha/scenario.js +++ b/lib/mocha/scenario.js @@ -40,7 +40,6 @@ module.exports.test = test => { return test } - test.steps = [] test.timeout(0) test.async = true diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index 3c559b12b..d31d76694 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -1,7 +1,4 @@ -/** - * Configuration for a test - * Can inject values and add custom configuration. - */ +/** @class */ class ScenarioConfig { constructor(test) { this.test = test @@ -32,12 +29,13 @@ class ScenarioConfig { } /** - * Retry this test for number of times + * Retry this test for x times * * @param {number} retries * @returns {this} */ retry(retries) { + if (process.env.SCENARIO_ONLY) retries = -retries this.test.retries(retries) return this } @@ -48,13 +46,17 @@ class ScenarioConfig { * @returns {this} */ timeout(timeout) { + console.log(`Scenario('${this.test.title}', () => {}).timeout(${timeout}) is deprecated!`) + console.log(`Please use Scenario('${this.test.title}', { timeout: ${timeout / 1000} }, () => {}) instead`) + console.log('Timeout should be set in seconds') + this.test.timeout(timeout) return this } /** * Pass in additional objects to inject into test - * @param {*} obj + * @param {Object} obj * @returns {this} */ inject(obj) { @@ -65,18 +67,22 @@ class ScenarioConfig { /** * Configures a helper. * Helper name can be omitted and values will be applied to first helper. - * @param {string|number} helper - * @param {*} obj + * @param {string | Object} helper + * @param {Object} [obj] * @returns {this} */ - config(helper, obj) { + async config(helper, obj) { if (!obj) { obj = helper helper = 0 } if (typeof obj === 'function') { - obj = obj(this.test) + obj(this.test).then(result => { + this.test.config[helper] = result + }) + return this } + if (!this.test.config) { this.test.config = {} } @@ -91,8 +97,8 @@ class ScenarioConfig { */ tag(tagName) { if (tagName[0] !== '@') tagName = `@${tagName}` - if (!this.test.tags) this.test.tags = [] this.test.tags.push(tagName) + this.test.title = `${this.test.title.trim()} ${tagName}` return this } diff --git a/lib/mocha/suite.js b/lib/mocha/suite.js new file mode 100644 index 000000000..be96439d6 --- /dev/null +++ b/lib/mocha/suite.js @@ -0,0 +1,55 @@ +const MochaSuite = require('mocha/lib/suite') + +/** + * @typedef {import('mocha')} Mocha + */ + +/** + * Enhances MochaSuite with CodeceptJS specific functionality using composition + */ +function enhanceMochaSuite(suite) { + // already enhanced + if (suite.codeceptjs) return suite + + suite.codeceptjs = true + // Add properties + suite.tags = suite.title.match(/(\@[a-zA-Z0-9-_]+)/g) || [] + suite.opts = {} + // suite.totalTimeout = undefined + + // Override fullTitle method + suite.fullTitle = () => `${suite.title}:` + + // Add new methods + suite.applyOptions = function (opts) { + if (!opts) opts = {} + suite.opts = opts + + if (opts.retries) suite.retries(opts.retries) + if (opts.timeout) suite.totalTimeout = opts.timeout + + if (opts.skipInfo && opts.skipInfo.skipped) { + suite.pending = true + suite.opts = { ...this.opts, skipInfo: opts.skipInfo } + } + } + + return suite +} + +/** + * Factory function to create enhanced suites + * @param {Mocha.Suite} parent - Parent suite + * @param {string} title - Suite title + * @returns {CodeceptJS.Suite & Mocha.Suite} New enhanced suite instance + */ +function createSuite(parent, title) { + const suite = MochaSuite.create(parent, title) + suite.timeout(0) + return enhanceMochaSuite(suite) +} + +module.exports = { + createSuite, + enhanceMochaSuite, +} diff --git a/lib/mocha/test.js b/lib/mocha/test.js new file mode 100644 index 000000000..0ca7cb926 --- /dev/null +++ b/lib/mocha/test.js @@ -0,0 +1,58 @@ +const Test = require('mocha/lib/test') +const scenario = require('./scenario') +const { enhanceMochaSuite } = require('./suite') + +/** + * Factory function to create enhanced tests + * @param {string} title - Test title + * @param {Function} fn - Test function + * @returns {CodeceptJS.Test & Mocha.Test} New enhanced test instance + */ +function createTest(title, fn) { + const test = new Test(title, fn) + return enhanceMochaTest(test) +} + +/** + * Enhances Mocha Test with CodeceptJS specific functionality using composition + * @param {CodeceptJS.Test & Mocha.Test} test - Test instance to enhance + * @returns {CodeceptJS.Test & Mocha.Test} Enhanced test instance + */ +function enhanceMochaTest(test) { + // already enhanced + if (test.codeceptjs) return test + + test.codeceptjs = true + // Add properties + test.tags = test.title.match(/(\@[a-zA-Z0-9-_]+)/g) || [] + test.steps = [] + test.config = {} + test.artifacts = [] + test.inject = {} + test.opts = {} + + // Add new methods + /** + * @param {Mocha.Suite} suite - The Mocha suite to add this test to + */ + test.addToSuite = function (suite) { + enhanceMochaSuite(suite) + suite.addTest(scenario.test(this)) + test.tags = [...(test.tags || []), ...(suite.tags || [])] + test.fullTitle = () => `${suite.title}: ${test.title}` + } + + test.applyOptions = function (opts) { + if (!opts) opts = {} + test.opts = opts + test.totalTimeout = opts.timeout + if (opts.retries) this.retries(opts.retries) + } + + return test +} + +module.exports = { + createTest, + enhanceMochaTest, +} diff --git a/lib/mocha/types.d.ts b/lib/mocha/types.d.ts new file mode 100644 index 000000000..36e3e72ce --- /dev/null +++ b/lib/mocha/types.d.ts @@ -0,0 +1,30 @@ +import { Test as MochaTest, Suite as MochaSuite } from 'mocha' + +declare global { + namespace CodeceptJS { + interface Test extends MochaTest { + title: string + tags: string[] + steps: string[] + config: Record + artifacts: string[] + inject: Record + opts: Record + throws?: Error | string | RegExp | Function + totalTimeout?: number + addToSuite(suite: Mocha.Suite): void + applyOptions(opts: Record): void + codeceptjs: boolean + } + + interface Suite extends MochaSuite { + title: string + tags: string[] + opts: Record + totalTimeout?: number + addTest(test: Test): void + applyOptions(opts: Record): void + codeceptjs: boolean + } + } +} diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js index 2c0eabad5..458a51037 100644 --- a/lib/mocha/ui.js +++ b/lib/mocha/ui.js @@ -1,14 +1,13 @@ const escapeRe = require('escape-string-regexp') -const Suite = require('mocha/lib/suite') -const Test = require('mocha/lib/test') - const scenario = require('./scenario') const ScenarioConfig = require('./scenarioConfig') const FeatureConfig = require('./featureConfig') const addDataContext = require('../data/context') -const container = require('../container') +const { createTest } = require('./test') +const { createSuite } = require('./suite') const setContextTranslation = context => { + const container = require('../container') const contexts = container.translation().value('contexts') if (contexts) { @@ -55,19 +54,10 @@ module.exports = function (suite) { if (suite.pending) { fn = null } - const test = new Test(title, fn) - test.fullTitle = () => `${suite.title}: ${test.title}` - - test.tags = (suite.tags || []).concat(title.match(/(\@[a-zA-Z0-9-_]+)/g) || []) // match tags from title + const test = createTest(title, fn) test.file = file - if (!test.inject) { - test.inject = {} - } - - suite.addTest(scenario.test(test)) - if (opts.retries) test.retries(opts.retries) - if (opts.timeout) test.totalTimeout = opts.timeout - test.opts = opts + test.addToSuite(suite) + test.applyOptions(opts) return new ScenarioConfig(test) } @@ -98,17 +88,10 @@ module.exports = function (suite) { afterAllHooksAreLoaded = false afterEachHooksAreLoaded = false - const suite = Suite.create(suites[0], title) - if (!opts) opts = {} - suite.opts = opts - suite.timeout(0) + const suite = createSuite(suites[0], title) + suite.applyOptions(opts) - if (opts.retries) suite.retries(opts.retries) - if (opts.timeout) suite.totalTimeout = opts.timeout - - suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || [] // match tags from title suite.file = file - suite.fullTitle = () => `${suite.title}:` suites.unshift(suite) suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]) @@ -116,11 +99,6 @@ module.exports = function (suite) { suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]) - if (opts.skipInfo && opts.skipInfo.skipped) { - suite.pending = true - suite.opts = { ...suite.opts, skipInfo: opts.skipInfo } - } - return new FeatureConfig(suite) } diff --git a/lib/store.js b/lib/store.js index a724d7ef2..7dfa03df8 100644 --- a/lib/store.js +++ b/lib/store.js @@ -9,6 +9,9 @@ const store = { timeouts: true, /** @type {boolean} */ dryRun: false, -}; -module.exports = store; + /** @type {CodeceptJS.Test | null} */ + currentTest: null, +} + +module.exports = store diff --git a/test/data/sandbox/configs/bootstrap/fs_test.js b/test/data/sandbox/configs/bootstrap/fs_test.js index 4feb17fa8..a54f8dcca 100644 --- a/test/data/sandbox/configs/bootstrap/fs_test.js +++ b/test/data/sandbox/configs/bootstrap/fs_test.js @@ -1,14 +1,18 @@ -Feature('Filesystem').tag('main'); +Feature('Filesystem').tag('main') Scenario('see content in file', ({ I }) => { - I.amInPath('.'); - I.say('hello world'); - I.seeFile('fs_test.js'); - I.seeFileContentsEqualReferenceFile(__filename); -}).tag('slow').tag('@important'); + I.amInPath('.') + I.say('hello world') + I.seeFile('fs_test.js') + I.seeFileContentsEqualReferenceFile(__filename) +}) + .tag('slow') + .tag('@important') Scenario('wait for file in current dir', ({ I }) => { - I.amInPath('.'); - I.say('hello world'); - I.waitForFile('fs_test.js'); -}).tag('slow').tag('@important'); + I.amInPath('.') + I.say('hello world') + I.waitForFile('fs_test.js') +}) + .tag('slow') + .tag('@important') diff --git a/test/runner/bootstrap_test.js b/test/runner/bootstrap_test.js index 129a4083e..c5237ee95 100644 --- a/test/runner/bootstrap_test.js +++ b/test/runner/bootstrap_test.js @@ -1,6 +1,7 @@ const assert = require('assert') const path = require('path') const exec = require('child_process').exec +const debug = require('debug')('codeceptjs:test') const runner = path.join(__dirname, '/../../bin/codecept.js') const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/bootstrap') @@ -12,6 +13,7 @@ describe('CodeceptJS Bootstrap and Teardown', () => { // success it('should run bootstrap', done => { exec(codecept_run_config('bootstrap.conf.js', '@important'), (err, stdout) => { + debug(stdout) stdout.should.include('Filesystem') // feature stdout.should.include('I am bootstrap') stdout.should.include('I am teardown') diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js index 8644f67ba..1759a610c 100644 --- a/test/runner/codecept_test.js +++ b/test/runner/codecept_test.js @@ -5,6 +5,7 @@ import('chai').then(chai => { const assert = require('assert') const path = require('path') const exec = require('child_process').exec +const debug = require('debug')('codeceptjs:test') const event = require('../../lib').event const runner = path.join(__dirname, '/../../bin/codecept.js') @@ -69,6 +70,7 @@ describe('CodeceptJS Runner', () => { it('filter by scenario tags', done => { process.chdir(codecept_dir) exec(`${codecept_run} --grep @slow`, (err, stdout) => { + debug(stdout) stdout.should.include('Filesystem') // feature stdout.should.include('check current dir') // test name assert(!err) diff --git a/test/runner/session_test.js b/test/runner/session_test.js index e869da641..636d53b9f 100644 --- a/test/runner/session_test.js +++ b/test/runner/session_test.js @@ -22,7 +22,7 @@ describe('CodeceptJS session', function () { testStatus = list.pop() testStatus.should.include('OK') list.should.eql( - ['I do "writing"', 'davert: I do "reading"', 'I do "playing"', 'john: I do "crying"', 'davert: I do "smiling"', 'I do "laughing"', 'mike: I do "spying"', 'john: I do "lying"', 'I do "waving"'], + ['Scenario()', 'I do "writing"', 'davert: I do "reading"', 'I do "playing"', 'john: I do "crying"', 'davert: I do "smiling"', 'I do "laughing"', 'mike: I do "spying"', 'john: I do "lying"', 'I do "waving"'], 'check steps execution order', ) done() @@ -38,7 +38,7 @@ describe('CodeceptJS session', function () { testStatus = list.pop() testStatus.should.include('OK') - list.should.eql(['I do "writing"', 'I do "playing"', 'john: I do "crying"', 'davert: I do "smiling"', 'I do "laughing"', 'davert: I do "singing"', 'I do "waving"'], 'check steps execution order') + list.should.eql(['Scenario()', 'I do "writing"', 'I do "playing"', 'john: I do "crying"', 'davert: I do "smiling"', 'I do "laughing"', 'davert: I do "singing"', 'I do "waving"'], 'check steps execution order') done() }) }) diff --git a/test/runner/within_test.js b/test/runner/within_test.js index 5bd947e91..707d9ce8d 100644 --- a/test/runner/within_test.js +++ b/test/runner/within_test.js @@ -23,7 +23,7 @@ describe('CodeceptJS within', function () { testStatus = withoutGeneratorList.pop() testStatus.should.include('OK') withoutGeneratorList.should.eql( - ['I small promise ', 'I small promise was finished ', 'I hey! i am within begin. i get blabla ', 'Within "blabla"', 'I small promise ', 'I small promise was finished ', 'I oh! i am within end( '], + ['Scenario()', 'I small promise ', 'I small promise was finished ', 'I hey! i am within begin. i get blabla ', 'Within "blabla"', 'I small promise ', 'I small promise was finished ', 'I oh! i am within end( '], 'check steps execution order', ) done() @@ -39,6 +39,7 @@ describe('CodeceptJS within', function () { testStatus.should.include('OK') withGeneratorList.should.eql( [ + 'Scenario()', 'I small promise ', 'I small promise was finished ', 'I small yield ', @@ -67,6 +68,7 @@ describe('CodeceptJS within', function () { testStatus.should.include('OK') withGeneratorList.should.eql( [ + 'Scenario()', 'I small promise ', 'I small promise was finished ', 'I small yield ', diff --git a/test/support/TestHelper.js b/test/support/TestHelper.js index 7e605fb20..b9fe08c7e 100644 --- a/test/support/TestHelper.js +++ b/test/support/TestHelper.js @@ -30,6 +30,12 @@ class TestHelper { static graphQLServerUrl() { return process.env.GRAPHQL_SERVER_URL || 'http://localhost:8020/graphql' } + + static echo(...args) { + if (!process.env.DEBUG) return + + console.log(...args) + } } module.exports = TestHelper diff --git a/test/unit/ui_test.js b/test/unit/ui_test.js index 85034f775..bcbb5a03c 100644 --- a/test/unit/ui_test.js +++ b/test/unit/ui_test.js @@ -7,12 +7,14 @@ const Suite = require('mocha/lib/suite') global.codeceptjs = require('../../lib') const makeUI = require('../../lib/mocha/ui') +const container = require('../../lib/container') describe('ui', () => { let suite let context beforeEach(() => { + container.clear() context = {} suite = new Suite('empty') makeUI(suite) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0a82ffd14..fb6a68aac 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,6 @@ // Project: https://github.com/codeception/codeceptjs/ /// +/// /// /// /// @@ -455,9 +456,7 @@ declare namespace CodeceptJS { } // Types who are not be defined by JSDoc - type actor = void }>( - customSteps?: T & ThisType>, - ) => WithTranslation + type actor = void }>(customSteps?: T & ThisType>) => WithTranslation type ILocator = | { id: string } From cfc95c6a92df8bd64fb19827fcb3e2cb41647f76 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 5 Jan 2025 04:57:43 +0200 Subject: [PATCH 10/13] fixed bdd tests --- lib/mocha/gherkin.js | 13 ++++++++----- lib/mocha/scenarioConfig.js | 15 ++++++++------- test/unit/bdd_test.js | 6 ++++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/mocha/gherkin.js b/lib/mocha/gherkin.js index 10b5d3b75..d7e7f9489 100644 --- a/lib/mocha/gherkin.js +++ b/lib/mocha/gherkin.js @@ -1,8 +1,10 @@ const Gherkin = require('@cucumber/gherkin') const Messages = require('@cucumber/messages') -const { Context, Suite, Test } = require('mocha') +const { Context, Suite } = require('mocha') const debug = require('debug')('codeceptjs:bdd') +const { enhanceMochaSuite } = require('./suite') +const { createTest } = require('./test') const { matchStep } = require('./bdd') const event = require('../event') const scenario = require('./scenario') @@ -28,6 +30,7 @@ module.exports = (text, file) => { throw new Error(`No 'Features' available in Gherkin '${file}' provided!`) } const suite = new Suite(ast.feature.name, new Context()) + enhanceMochaSuite(suite) const tags = ast.feature.tags.map(t => t.name) suite.title = `${suite.title} ${tags.join(' ')}`.trim() suite.tags = tags || [] @@ -130,10 +133,10 @@ module.exports = (text, file) => { } } - const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))) + const test = createTest(title, async () => runSteps(addExampleInTable(exampleSteps, current))) + test.addToSuite(suite) test.tags = suite.tags.concat(tags) test.file = file - suite.addTest(scenario.test(test)) } } continue @@ -142,10 +145,10 @@ module.exports = (text, file) => { if (child.scenario) { const tags = child.scenario.tags.map(t => t.name) const title = `${child.scenario.name} ${tags.join(' ')}`.trim() - const test = new Test(title, async () => runSteps(child.scenario.steps)) + const test = createTest(title, async () => runSteps(child.scenario.steps)) + test.addToSuite(suite) test.tags = suite.tags.concat(tags) test.file = file - suite.addTest(scenario.test(test)) } } diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index d31d76694..cd46b37bc 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -1,3 +1,5 @@ +const { isAsyncFunction } = require('../utils') + /** @class */ class ScenarioConfig { constructor(test) { @@ -71,21 +73,20 @@ class ScenarioConfig { * @param {Object} [obj] * @returns {this} */ - async config(helper, obj) { + config(helper, obj) { if (!obj) { obj = helper helper = 0 } if (typeof obj === 'function') { - obj(this.test).then(result => { - this.test.config[helper] = result - }) + if (isAsyncFunction(obj)) { + obj(this.test).then(res => (this.test.config[helper] = res)) + } else { + obj = obj(this.test) + } return this } - if (!this.test.config) { - this.test.config = {} - } this.test.config[helper] = obj return this } diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index f41c4fc79..537cc9b9b 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -1,6 +1,6 @@ const Gherkin = require('@cucumber/gherkin') const Messages = require('@cucumber/messages') - +const path = require('path') const chai = require('chai') const expect = chai.expect @@ -17,6 +17,8 @@ const container = require('../../lib/container') const actor = require('../../lib/actor') const event = require('../../lib/event') +global.codecept_dir = path.join(__dirname, '/..') + class Color { constructor(name) { this.name = name @@ -111,7 +113,7 @@ describe('BDD', () => { expect('@super').is.equal(suite.tests[0].tags[0]) }) - it('should load step definitions', done => { + it('should load and run step definitions', done => { let sum = 0 Given(/I have product with (\d+) price/, param => (sum += parseInt(param, 10))) When('I go to checkout process', () => (sum += 10)) From 1ca2470daa4eed10bd397c3e607f5921aee5c0a5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 5 Jan 2025 05:18:42 +0200 Subject: [PATCH 11/13] added hook config --- lib/mocha/hooks.js | 42 ++++++++++++++++++++++++++++++++----- lib/mocha/scenarioConfig.js | 5 ++--- lib/mocha/ui.js | 5 +++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index dd3dbda26..a44ddb716 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -4,7 +4,7 @@ class Hook { constructor(context, error) { this.suite = context.suite this.test = context.test - this.runnable = context.ctx.test + this.runnable = context?.ctx?.test this.ctx = context.ctx this.error = error } @@ -17,6 +17,10 @@ class Hook { return this.toString() + '()' } + retry(n) { + // must be implemented for each hook + } + get title() { return this.ctx?.test?.title || this.name } @@ -26,13 +30,29 @@ class Hook { } } -class BeforeHook extends Hook {} +class BeforeHook extends Hook { + retry(n) { + this.suite.opts['retryBefore'] = n + } +} -class AfterHook extends Hook {} +class AfterHook extends Hook { + retry(n) { + this.suite.opts['retryAfter'] = n + } +} -class BeforeSuiteHook extends Hook {} +class BeforeSuiteHook extends Hook { + retry(n) { + this.suite.opts['retryBeforeSuite'] = n + } +} -class AfterSuiteHook extends Hook {} +class AfterSuiteHook extends Hook { + retry(n) { + this.suite.opts['retryAfterSuite'] = n + } +} function fireHook(eventType, suite, error) { const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1] @@ -54,10 +74,22 @@ function fireHook(eventType, suite, error) { } } +class HookConfig { + constructor(hook) { + this.hook = hook + } + + retry(n) { + this.hook.retry(n) + return this + } +} + module.exports = { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook, fireHook, + HookConfig, } diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index cd46b37bc..33ca01f28 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -81,10 +81,9 @@ class ScenarioConfig { if (typeof obj === 'function') { if (isAsyncFunction(obj)) { obj(this.test).then(res => (this.test.config[helper] = res)) - } else { - obj = obj(this.test) + return this } - return this + obj = obj(this.test) } this.test.config[helper] = obj diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js index 458a51037..93686701c 100644 --- a/lib/mocha/ui.js +++ b/lib/mocha/ui.js @@ -5,6 +5,7 @@ const FeatureConfig = require('./featureConfig') const addDataContext = require('../data/context') const { createTest } = require('./test') const { createSuite } = require('./suite') +const { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } = require('./hooks') const setContextTranslation = context => { const container = require('../container') @@ -118,18 +119,22 @@ module.exports = function (suite) { context.BeforeSuite = function (fn) { suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')) + return new HookConfig(new BeforeSuiteHook({ suite: suites[0] })) } context.AfterSuite = function (fn) { afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]) + return new HookConfig(new AfterSuiteHook({ suite: suites[0] })) } context.Background = context.Before = function (fn) { suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')) + return new HookConfig(new BeforeHook({ suite: suites[0] })) } context.After = function (fn) { afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]) + return new HookConfig(new AfterHook({ suite: suites[0] })) } /** From 04629585457a06cd62077710f6763844e7a7caab Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 5 Jan 2025 05:19:38 +0200 Subject: [PATCH 12/13] fixed lint --- lib/mocha/hooks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index a44ddb716..ab5d5a1d1 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -1,3 +1,4 @@ +/* eslint-disable dot-notation */ const event = require('../event') class Hook { From 479d42f2d344a5d5bfad1766f8983dbedec6f271 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 5 Jan 2025 15:53:42 +0200 Subject: [PATCH 13/13] refactored tests --- docs/basics.md | 426 +++++++++--------- lib/mocha/{scenario.js => asyncWrapper.js} | 1 + lib/mocha/gherkin.js | 12 +- lib/mocha/hooks.js | 32 +- lib/mocha/test.js | 4 +- lib/mocha/ui.js | 18 +- .../codecept.retry.hookconfig.conf.js | 12 + .../retryHooks/retry_async_hook_test2.js | 18 + test/runner/retry_hooks_test.js | 8 + .../asyncWrapper_test.js} | 30 +- test/unit/{ => mocha}/ui_test.js | 6 +- 11 files changed, 302 insertions(+), 265 deletions(-) rename lib/mocha/{scenario.js => asyncWrapper.js} (99%) create mode 100644 test/data/sandbox/configs/retryHooks/codecept.retry.hookconfig.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_async_hook_test2.js rename test/unit/{scenario_test.js => mocha/asyncWrapper_test.js} (79%) rename test/unit/{ => mocha}/ui_test.js (98%) diff --git a/docs/basics.md b/docs/basics.md index 6904c78d9..39134b4b7 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -8,12 +8,12 @@ title: Getting Started CodeceptJS is a modern end to end testing framework with a special BDD-style syntax. The tests are written as a linear scenario of the user's action on a site. ```js -Feature('CodeceptJS demo'); +Feature('CodeceptJS demo') Scenario('check Welcome page on site', ({ I }) => { - I.amOnPage('/'); - I.see('Welcome'); -}); + I.amOnPage('/') + I.see('Welcome') +}) ``` Tests are expected to be written in **ECMAScript 7**. @@ -38,10 +38,10 @@ However, because of the difference in backends and their limitations, they are n Refer to following guides to more information on: -* [▶ Playwright](/playwright) -* [▶ WebDriver](/webdriver) -* [▶ Puppeteer](/puppeteer) -* [▶ TestCafe](/testcafe) +- [▶ Playwright](/playwright) +- [▶ WebDriver](/webdriver) +- [▶ Puppeteer](/puppeteer) +- [▶ TestCafe](/testcafe) > ℹ Depending on a helper selected a list of available actions may change. @@ -50,15 +50,14 @@ or enable [auto-completion by generating TypeScript definitions](#intellisense). > 🤔 It is possible to access API of a backend you use inside a test or a [custom helper](/helpers/). For instance, to use Puppeteer API inside a test use [`I.usePuppeteerTo`](/helpers/Puppeteer/#usepuppeteerto) inside a test. Similar methods exist for each helper. - ## Writing Tests Tests are written from a user's perspective. There is an actor (represented as `I`) which contains actions taken from helpers. A test is written as a sequence of actions performed by an actor: ```js -I.amOnPage('/'); -I.click('Login'); -I.see('Please Login', 'h1'); +I.amOnPage('/') +I.click('Login') +I.see('Please Login', 'h1') // ... ``` @@ -70,40 +69,39 @@ Start a test by opening a page. Use the `I.amOnPage()` command for this: ```js // When "http://site.com" is url in config -I.amOnPage('/'); // -> opens http://site.com/ -I.amOnPage('/about'); // -> opens http://site.com/about -I.amOnPage('https://google.com'); // -> https://google.com +I.amOnPage('/') // -> opens http://site.com/ +I.amOnPage('/about') // -> opens http://site.com/about +I.amOnPage('https://google.com') // -> https://google.com ``` When an URL doesn't start with a protocol (http:// or https://) it is considered to be a relative URL and will be appended to the URL which was initially set-up in the config. > It is recommended to use a relative URL and keep the base URL in the config file, so you can easily switch between development, stage, and production environments. - ### Locating Element Element can be found by CSS or XPath locators. ```js -I.seeElement('.user'); // element with CSS class user -I.seeElement('//button[contains(., "press me")]'); // button +I.seeElement('.user') // element with CSS class user +I.seeElement('//button[contains(., "press me")]') // button ``` By default CodeceptJS tries to guess the locator type. In order to specify the exact locator type you can pass an object called **strict locator**. ```js -I.seeElement({css: 'div.user'}); -I.seeElement({xpath: '//div[@class=user]'}); +I.seeElement({ css: 'div.user' }) +I.seeElement({ xpath: '//div[@class=user]' }) ``` Strict locators allow to specify additional locator types: ```js // locate form element by name -I.seeElement({name: 'password'}); +I.seeElement({ name: 'password' }) // locate element by React component and props -I.seeElement({react: 'user-profile', props: {name: 'davert'}}); +I.seeElement({ react: 'user-profile', props: { name: 'davert' } }) ``` In [mobile testing](https://codecept.io/mobile/#locating-elements) you can use `~` to specify the accessibility id to locate an element. In web application you can locate elements by their `aria-label` value. @@ -111,7 +109,7 @@ In [mobile testing](https://codecept.io/mobile/#locating-elements) you can use ` ```js // locate element by [aria-label] attribute in web // or by accessibility id in mobile -I.seeElement('~username'); +I.seeElement('~username') ``` > [▶ Learn more about using locators in CodeceptJS](/locators). @@ -124,7 +122,7 @@ By default CodeceptJS tries to find the button or link with the exact text on it ```js // search for link or button -I.click('Login'); +I.click('Login') ``` If none was found, CodeceptJS tries to find a link or button containing that text. In case an image is clickable its `alt` attribute will be checked for text inclusion. Form buttons will also be searched by name. @@ -132,8 +130,8 @@ If none was found, CodeceptJS tries to find a link or button containing that tex To narrow down the results you can specify a context in the second parameter. ```js -I.click('Login', '.nav'); // search only in .nav -I.click('Login', {css: 'footer'}); // search only in footer +I.click('Login', '.nav') // search only in .nav +I.click('Login', { css: 'footer' }) // search only in footer ``` > To skip guessing the locator type, pass in a strict locator - A locator starting with '#' or '.' is considered to be CSS. Locators starting with '//' or './/' are considered to be XPath. @@ -142,9 +140,9 @@ You are not limited to buttons and links. Any element can be found by passing in ```js // click element by CSS -I.click('#signup'); +I.click('#signup') // click element located by special test-id attribute -I.click('//dev[@test-id="myid"]'); +I.click('//dev[@test-id="myid"]') ``` > ℹ If click doesn't work in a test but works for user, it is possible that frontend application is not designed for automated testing. To overcome limitation of standard click in this edgecase use `forceClick` method. It will emulate click instead of sending native event. This command will click an element no matter if this element is visible or animating. It will send JavaScript "click" event to it. @@ -159,19 +157,19 @@ Let's submit this sample form for a test: ```html
- -
- -
- -
- -
- -
+ +
+ +
+ +
+ +
+ +
``` @@ -179,14 +177,14 @@ We need to fill in all those fields and click the "Update" button. CodeceptJS ma ```js // we are using label to match user_name field -I.fillField('Name', 'Miles'); +I.fillField('Name', 'Miles') // we can use input name -I.fillField('user[email]','miles@davis.com'); +I.fillField('user[email]', 'miles@davis.com') // select element by label, choose option by text -I.selectOption('Role','Admin'); +I.selectOption('Role', 'Admin') // click 'Save' button, found by text -I.checkOption('Accept'); -I.click('Save'); +I.checkOption('Accept') +I.click('Save') ``` > ℹ `selectOption` works only with standard ` HTML elements. If your selectbox is created by React, Vue, or as a component of any other framework, this method potentially won't work with it. Use `click` to manipulate it. @@ -197,18 +195,18 @@ Alternative scenario: ```js // we are using CSS -I.fillField('#user_name', 'Miles'); -I.fillField('#user_email','miles@davis.com'); +I.fillField('#user_name', 'Miles') +I.fillField('#user_email', 'miles@davis.com') // select element by label, option by value -I.selectOption('#user_role','1'); +I.selectOption('#user_role', '1') // click 'Update' button, found by name -I.click('submitButton', '#update_form'); +I.click('submitButton', '#update_form') ``` To fill in sensitive data use the `secret` function, it won't expose actual value in logs. ```js -I.fillField('password', secret('123456')); +I.fillField('password', secret('123456')) ``` > ℹ️ Learn more about [masking secret](/secrets/) output @@ -222,11 +220,11 @@ The most general and common assertion is `see`, which checks visilibility of a t ```js // Just a visible text on a page -I.see('Hello'); +I.see('Hello') // text inside .msg element -I.see('Hello', '.msg'); +I.see('Hello', '.msg') // opposite -I.dontSee('Bye'); +I.dontSee('Bye') ``` You should provide a text as first argument and, optionally, a locator to search for a text in a context. @@ -234,16 +232,16 @@ You should provide a text as first argument and, optionally, a locator to search You can check that specific element exists (or not) on a page, as it was described in [Locating Element](#locating-element) section. ```js -I.seeElement('.notice'); -I.dontSeeElement('.error'); +I.seeElement('.notice') +I.dontSeeElement('.error') ``` Additional assertions: ```js -I.seeInCurrentUrl('/user/miles'); -I.seeInField('user[name]', 'Miles'); -I.seeInTitle('My Website'); +I.seeInCurrentUrl('/user/miles') +I.seeInField('user[name]', 'Miles') +I.seeInTitle('My Website') ``` To see all possible assertions, check the helper's reference. @@ -257,15 +255,15 @@ Imagine the application generates a password, and you want to ensure that user c ```js Scenario('login with generated password', async ({ I }) => { - I.fillField('email', 'miles@davis.com'); - I.click('Generate Password'); - const password = await I.grabTextFrom('#password'); - I.click('Login'); - I.fillField('email', 'miles@davis.com'); - I.fillField('password', password); - I.click('Log in!'); - I.see('Hello, Miles'); -}); + I.fillField('email', 'miles@davis.com') + I.click('Generate Password') + const password = await I.grabTextFrom('#password') + I.click('Login') + I.fillField('email', 'miles@davis.com') + I.fillField('password', password) + I.click('Log in!') + I.see('Hello, Miles') +}) ``` The `grabTextFrom` action is used to retrieve the text from an element. All actions starting with the `grab` prefix are expected to return data. In order to synchronize this step with a scenario you should pause the test execution with the `await` keyword of ES6. To make it work, your test should be written inside a async function (notice `async` in its definition). @@ -273,9 +271,9 @@ The `grabTextFrom` action is used to retrieve the text from an element. All acti ```js Scenario('use page title', async ({ I }) => { // ... - const password = await I.grabTextFrom('#password'); - I.fillField('password', password); -}); + const password = await I.grabTextFrom('#password') + I.fillField('password', password) +}) ``` ### Waiting @@ -285,9 +283,9 @@ Sometimes that may cause delays. A test may fail while trying to click an elemen To handle these cases, the `wait*` methods has been introduced. ```js -I.waitForElement('#agree_button', 30); // secs +I.waitForElement('#agree_button', 30) // secs // clicks a button only when it is visible -I.click('#agree_button'); +I.click('#agree_button') ``` ## How It Works @@ -304,16 +302,16 @@ If you want to get information from a running test you can use `await` inside th ```js Scenario('try grabbers', async ({ I }) => { - let title = await I.grabTitle(); -}); + let title = await I.grabTitle() +}) ``` then you can use those variables in assertions: ```js -var title = await I.grabTitle(); -var assert = require('assert'); -assert.equal(title, 'CodeceptJS'); +var title = await I.grabTitle() +var assert = require('assert') +assert.equal(title, 'CodeceptJS') ``` It is important to understand the usage of **async** functions in CodeceptJS. While non-returning actions can be called without await, if an async function uses `grab*` action it must be called with `await`: @@ -321,15 +319,15 @@ It is important to understand the usage of **async** functions in CodeceptJS. Wh ```js // a helper function async function getAllUsers(I) { - const users = await I.grabTextFrom('.users'); - return users.filter(u => u.includes('active')) + const users = await I.grabTextFrom('.users') + return users.filter(u => u.includes('active')) } // a test Scenario('try helper functions', async ({ I }) => { // we call function with await because it includes `grab` - const users = await getAllUsers(I); -}); + const users = await getAllUsers(I) +}) ``` If you miss `await` you get commands unsynchrhonized. And this will result to an error like this: @@ -388,7 +386,6 @@ npx codeceptjs run --grep "slow" It is recommended to [filter tests by tags](/advanced/#tags). - > For more options see [full reference of `run` command](/commands/#run). ### Parallel Run @@ -416,7 +413,7 @@ exports.config = { }, include: { // current actor and page objects - } + }, } ``` @@ -433,12 +430,12 @@ Tuning configuration for helpers like WebDriver, Puppeteer can be hard, as it re For instance, you can set the window size or toggle headless mode, no matter of which helpers are actually used. ```js -const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure'); +const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure') // run headless when CI environment variable set -setHeadlessWhen(process.env.CI); +setHeadlessWhen(process.env.CI) // set window size for any helper: Puppeteer, WebDriver, TestCafe -setWindowSize(1600, 1200); +setWindowSize(1600, 1200) exports.config = { // ... @@ -455,8 +452,8 @@ By using the interactive shell you can stop execution at any point and type in a This is especially useful while writing a new scratch. After opening a page call `pause()` to start interacting with a page: ```js -I.amOnPage('/'); -pause(); +I.amOnPage('/') +pause() ``` Try to perform your scenario step by step. Then copy succesful commands and insert them into a test. @@ -492,7 +489,7 @@ To see all available commands, press TAB two times to see list of all actions in PageObjects and other variables can also be passed to as object: ```js -pause({ loginPage, data: 'hi', func: () => console.log('hello') }); +pause({ loginPage, data: 'hi', func: () => console.log('hello') }) ``` Inside a pause mode you can use `loginPage`, `data`, `func` variables. @@ -519,7 +516,6 @@ npx codeceptjs run -p pauseOnFail > To enable pause after a test without a plugin you can use `After(pause)` inside a test file. - ### Screenshot on Failure By default CodeceptJS saves a screenshot of a failed test. @@ -536,21 +532,22 @@ To see how the test was executed, use [stepByStepReport Plugin](/plugins/#stepby Common preparation steps like opening a web page or logging in a user, can be placed in the `Before` or `Background` hooks: ```js -Feature('CodeceptJS Demonstration'); +Feature('CodeceptJS Demonstration') -Before(({ I }) => { // or Background - I.amOnPage('/documentation'); -}); +Before(({ I }) => { + // or Background + I.amOnPage('/documentation') +}) Scenario('test some forms', ({ I }) => { - I.click('Create User'); - I.see('User is valid'); - I.dontSeeInCurrentUrl('/documentation'); -}); + I.click('Create User') + I.see('User is valid') + I.dontSeeInCurrentUrl('/documentation') +}) Scenario('test title', ({ I }) => { - I.seeInTitle('Example application'); -}); + I.seeInTitle('Example application') +}) ``` Same as `Before` you can use `After` to run teardown for each scenario. @@ -563,13 +560,13 @@ You can use them to execute handlers that will setup your environment. `BeforeSu ```js BeforeSuite(({ I }) => { - I.syncDown('testfolder'); -}); + I.syncDown('testfolder') +}) AfterSuite(({ I }) => { - I.syncUp('testfolder'); - I.clearDir('testfolder'); -}); + I.syncUp('testfolder') + I.clearDir('testfolder') +}) ``` ## Retries @@ -577,7 +574,7 @@ AfterSuite(({ I }) => { ### Auto Retry Each failed step is auto-retried by default via [retryFailedStep Plugin](/plugins/#retryfailedstep). -If this is not expected, this plugin can be disabled in a config. +If this is not expected, this plugin can be disabled in a config. > **[retryFailedStep plugin](/plugins/#retryfailedstep) is enabled by default** incide global configuration @@ -589,30 +586,29 @@ If you have a step which often fails, you can retry execution for this single st Use the `retry()` function before an action to ask CodeceptJS to retry it on failure: ```js -I.retry().see('Welcome'); +I.retry().see('Welcome') ``` If you'd like to retry a step more than once, pass the amount as a parameter: ```js -I.retry(3).see('Welcome'); +I.retry(3).see('Welcome') ``` Additional options can be provided to `retry`, so you can set the additional options (defined in [promise-retry](https://www.npmjs.com/package/promise-retry) library). - ```js // retry action 3 times waiting for 0.1 second before next try -I.retry({ retries: 3, minTimeout: 100 }).see('Hello'); +I.retry({ retries: 3, minTimeout: 100 }).see('Hello') // retry action 3 times waiting no more than 3 seconds for last retry -I.retry({ retries: 3, maxTimeout: 3000 }).see('Hello'); +I.retry({ retries: 3, maxTimeout: 3000 }).see('Hello') // retry 2 times if error with message 'Node not visible' happens I.retry({ retries: 2, - when: err => err.message === 'Node not visible' -}).seeElement('#user'); + when: err => err.message === 'Node not visible', +}).seeElement('#user') ``` Pass a function to the `when` option to retry only when an error matches the expected one. @@ -623,11 +619,11 @@ To retry a group of steps enable [retryTo plugin](/plugins/#retryto): ```js // retry these steps 5 times before failing -await retryTo((tryNum) => { - I.switchTo('#editor frame'); - I.click('Open'); +await retryTo(tryNum => { + I.switchTo('#editor frame') + I.click('Open') I.see('Opened') -}, 5); +}, 5) ``` ### Retry Scenario @@ -640,38 +636,57 @@ You can set the number of a retries for a feature: ```js Scenario('Really complex', ({ I }) => { // test goes here -}).retry(2); +}).retry(2) // alternative -Scenario('Really complex', { retries: 2 },({ I }) => {}); +Scenario('Really complex', { retries: 2 }, ({ I }) => {}) ``` This scenario will be restarted two times on a failure. Unlike retry step, there is no `when` condition supported for retries on a scenario level. -### Retry Before +### Retry Before + +To retry `Before`, `BeforeSuite`, `After`, `AfterSuite` hooks, call `retry()` after declaring the hook. + +- `Before().retry()` +- `BeforeSuite().retry()` +- `After().retry()` +- `AfterSuite().retry()` -To retry `Before`, `BeforeSuite`, `After`, `AfterSuite` hooks, add corresponding option to a `Feature`: +For instance, to retry Before hook 3 times before failing: -* `retryBefore` -* `retryBeforeSuite` -* `retryAfter` -* `retryAfterSuite` +```js +Before(({ I }) => { + I.amOnPage('/') +}).retry(3) +``` -For instance, to retry Before hook 3 times: +Same applied for `BeforeSuite`: ```js -Feature('this have a flaky Befure', { retryBefore: 3 }) +BeforeSuite(() => { + // do some prepreations +}).retry(3) ``` -Multiple options of different values can be set at the same time +Alternatively, retry options can be set on Feature level: + +```js +Feature('my tests', { + retryBefore: 3, + retryBeforeSuite: 2, + retryAfter: 1, + retryAfterSuite: 3, +}) +``` ### Retry Feature To set this option for all scenarios in a file, add `retry` to a feature: ```js -Feature('Complex JS Stuff').retry(3); +Feature('Complex JS Stuff').retry(3) // or Feature('Complex JS Stuff', { retries: 3 }) ``` @@ -701,7 +716,7 @@ retry: { Before: ..., BeforeSuite: ..., After: ..., - AfterSuite: ..., + AfterSuite: ..., } ``` @@ -712,32 +727,32 @@ Multiple retry configs can be added via array. To use different retry configs fo retry: [ { // enable this config only for flaky tests - grep: '@flaky', + grep: '@flaky', Before: 3 Scenario: 3 - }, + }, { // retry less when running slow tests - grep: '@slow' + grep: '@slow' Scenario: 1 Before: 1 }, { - // retry all BeforeSuite + // retry all BeforeSuite BeforeSuite: 3 } ] ``` -When using `grep` with `Before`, `After`, `BeforeSuite`, `AfterSuite`, a suite title will be checked for included value. +When using `grep` with `Before`, `After`, `BeforeSuite`, `AfterSuite`, a suite title will be checked for included value. > ℹ️ `grep` value can be string or regexp Rules are applied in the order of array element, so the last option will override a previous one. Global retries config can be overridden in a file as described previously. -### Retry Run +### Retry Run On the highest level of the "retry pyramid" there is an option to retry a complete run multiple times. -Even this is the slowest option of all, it can be helpful to detect flaky tests. +Even this is the slowest option of all, it can be helpful to detect flaky tests. [`run-rerun`](https://codecept.io/commands/#run-rerun) command will restart the run multiple times to values you provide. You can set minimal and maximal number of restarts in configuration file. @@ -745,7 +760,6 @@ Even this is the slowest option of all, it can be helpful to detect flaky tests. npx codeceptjs run-rerun ``` - [Here are some ideas](https://github.com/codeceptjs/CodeceptJS/pull/231#issuecomment-249554933) on where to use BeforeSuite hooks. ## Within @@ -756,14 +770,14 @@ Everything executed in its context will be narrowed to context specified by loca Usage: `within('section', ()=>{})` ```js -I.amOnPage('https://github.com'); +I.amOnPage('https://github.com') within('.js-signup-form', () => { - I.fillField('user[login]', 'User'); - I.fillField('user[email]', 'user@user.com'); - I.fillField('user[password]', 'user@user.com'); - I.click('button'); -}); -I.see('There were problems creating your account.'); + I.fillField('user[login]', 'User') + I.fillField('user[email]', 'user@user.com') + I.fillField('user[password]', 'user@user.com') + I.click('button') +}) +I.see('There were problems creating your account.') ``` > ⚠ `within` can cause problems when used incorrectly. If you see a weird behavior of a test try to refactor it to not use `within`. It is recommended to keep within for simplest cases when possible. @@ -771,23 +785,22 @@ I.see('There were problems creating your account.'); `within` can also work with IFrames. A special `frame` locator is required to locate the iframe and get into its context. - See example: ```js -within({frame: "#editor"}, () => { - I.see('Page'); -}); +within({ frame: '#editor' }, () => { + I.see('Page') +}) ``` > ℹ IFrames can also be accessed via `I.switchTo` command of a corresponding helper. -Nested IFrames can be set by passing an array *(WebDriver & Puppeteer only)*: +Nested IFrames can be set by passing an array _(WebDriver & Puppeteer only)_: ```js -within({frame: [".content", "#editor"]}, () => { - I.see('Page'); -}); +within({ frame: ['.content', '#editor'] }, () => { + I.see('Page') +}) ``` When running steps inside, a within block will be shown with a shift: @@ -799,26 +812,26 @@ Within can return a value, which can be used in a scenario: ```js // inside async function const val = await within('#sidebar', () => { - return I.grabTextFrom({ css: 'h1' }); -}); -I.fillField('Description', val); + return I.grabTextFrom({ css: 'h1' }) +}) +I.fillField('Description', val) ``` ## Conditional Actions -There is a way to execute unsuccessful actions to without failing a test. +There is a way to execute unsuccessful actions to without failing a test. This might be useful when you might need to click "Accept cookie" button but probably cookies were already accepted. To handle these cases `tryTo` function was introduced: ```js -tryTo(() => I.click('Accept', '.cookies')); +tryTo(() => I.click('Accept', '.cookies')) ``` You may also use `tryTo` for cases when you deal with uncertainty on page: -* A/B testing -* soft assertions -* cookies & gdpr +- A/B testing +- soft assertions +- cookies & gdpr `tryTo` function is enabled by default via [tryTo plugin](/plugins/#tryto) @@ -828,20 +841,19 @@ There is a simple way to add additional comments to your test scenario: Use the `say` command to print information to screen: ```js -I.say('I am going to publish post'); -I.say('I enter title and body'); -I.say('I expect post is visible on site'); +I.say('I am going to publish post') +I.say('I enter title and body') +I.say('I expect post is visible on site') ``` Use the second parameter to pass in a color value (ASCII). ```js -I.say('This is red', 'red'); //red is used -I.say('This is blue', 'blue'); //blue is used -I.say('This is by default'); //cyan is used +I.say('This is red', 'red') //red is used +I.say('This is blue', 'blue') //blue is used +I.say('This is by default') //cyan is used ``` - ## IntelliSense ![Edit](/img/edit.gif) @@ -867,33 +879,32 @@ Create a file called `jsconfig.json` in your project root directory, unless you Alternatively, you can include `/// ` into your test files to get method autocompletion while writing tests. - ## Multiple Sessions CodeceptJS allows to run several browser sessions inside a test. This can be useful for testing communication between users inside a chat or other systems. To open another browser use the `session()` function as shown in the example: ```js Scenario('test app', ({ I }) => { - I.amOnPage('/chat'); - I.fillField('name', 'davert'); - I.click('Sign In'); - I.see('Hello, davert'); + I.amOnPage('/chat') + I.fillField('name', 'davert') + I.click('Sign In') + I.see('Hello, davert') session('john', () => { // another session started - I.amOnPage('/chat'); - I.fillField('name', 'john'); - I.click('Sign In'); - I.see('Hello, john'); - }); + I.amOnPage('/chat') + I.fillField('name', 'john') + I.click('Sign In') + I.see('Hello, john') + }) // switching back to default session - I.fillField('message', 'Hi, john'); + I.fillField('message', 'Hi, john') // there is a message from current user - I.see('me: Hi, john', '.messages'); + I.see('me: Hi, john', '.messages') session('john', () => { // let's check if john received it - I.see('davert: Hi, john', '.messages'); - }); -}); + I.see('davert: Hi, john', '.messages') + }) +}) ``` The `session` function expects the first parameter to be the name of the session. You can switch back to this session by using the same name. @@ -901,10 +912,10 @@ The `session` function expects the first parameter to be the name of the session You can override the configuration for the session by passing a second parameter: ```js -session('john', { browser: 'firefox' } , () => { +session('john', { browser: 'firefox' }, () => { // run this steps in firefox - I.amOnPage('/'); -}); + I.amOnPage('/') +}) ``` or just start the session without switching to it. Call `session` passing only its name: @@ -924,15 +935,16 @@ Scenario('test', ({ I }) => { }); } ``` + `session` can return a value which can be used in a scenario: ```js // inside async function const val = await session('john', () => { - I.amOnPage('/info'); - return I.grabTextFrom({ css: 'h1' }); -}); -I.fillField('Description', val); + I.amOnPage('/info') + return I.grabTextFrom({ css: 'h1' }) +}) +I.fillField('Description', val) ``` Functions passed into a session can use the `I` object, page objects, and any other objects declared for the scenario. @@ -940,17 +952,15 @@ This function can also be declared as async (but doesn't work as generator). Also, you can use `within` inside a session, but you can't call session from inside `within`. - ## Skipping Like in Mocha you can use `x` and `only` to skip tests or to run a single test. -* `xScenario` - skips current test -* `Scenario.skip` - skips current test -* `Scenario.only` - executes only the current test -* `xFeature` - skips current suite -* `Feature.skip` - skips the current suite - +- `xScenario` - skips current test +- `Scenario.skip` - skips current test +- `Scenario.only` - executes only the current test +- `xFeature` - skips current suite +- `Feature.skip` - skips the current suite ## Todo Test @@ -961,19 +971,19 @@ This test will be skipped like with regular `Scenario.skip` but with additional Use it with a test body as a test plan: ```js -Scenario.todo('Test', I => { -/** - * 1. Click to field - * 2. Fill field - * - * Result: - * 3. Field contains text - */ -}); +Scenario.todo('Test', I => { + /** + * 1. Click to field + * 2. Fill field + * + * Result: + * 3. Field contains text + */ +}) ``` Or even without a test body: ```js -Scenario.todo('Test'); +Scenario.todo('Test') ``` diff --git a/lib/mocha/scenario.js b/lib/mocha/asyncWrapper.js similarity index 99% rename from lib/mocha/scenario.js rename to lib/mocha/asyncWrapper.js index 8609bea3b..68902a912 100644 --- a/lib/mocha/scenario.js +++ b/lib/mocha/asyncWrapper.js @@ -29,6 +29,7 @@ function makeDoneCallableOnce(done) { return done(err) } } + /** * Wraps test function, injects support objects from container, * starts promise chain with recorder, performs before/after hooks diff --git a/lib/mocha/gherkin.js b/lib/mocha/gherkin.js index d7e7f9489..2e4d040fe 100644 --- a/lib/mocha/gherkin.js +++ b/lib/mocha/gherkin.js @@ -7,7 +7,7 @@ const { enhanceMochaSuite } = require('./suite') const { createTest } = require('./test') const { matchStep } = require('./bdd') const event = require('../event') -const scenario = require('./scenario') +const { injected, setup, teardown, suiteSetup, suiteTeardown } = require('./asyncWrapper') const Step = require('../step') const DataTableArgument = require('../data/dataTableArgument') const transform = require('../transform') @@ -39,10 +39,10 @@ module.exports = (text, file) => { suite.file = file suite.timeout(0) - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) - suite.afterEach('codeceptjs.after', () => scenario.teardown(suite)) - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) - suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)) + suite.beforeEach('codeceptjs.before', () => setup(suite)) + suite.afterEach('codeceptjs.after', () => teardown(suite)) + suite.beforeAll('codeceptjs.beforeSuite', () => suiteSetup(suite)) + suite.afterAll('codeceptjs.afterSuite', () => suiteTeardown(suite)) const runSteps = async steps => { for (const step of steps) { @@ -103,7 +103,7 @@ module.exports = (text, file) => { if (child.background) { suite.beforeEach( 'Before', - scenario.injected(async () => runSteps(child.background.steps), suite, 'before'), + injected(async () => runSteps(child.background.steps), suite, 'before'), ) continue } diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index ab5d5a1d1..ad0695d77 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -10,16 +10,20 @@ class Hook { this.error = error } - toString() { + get hookName() { return this.constructor.name.replace('Hook', '') } + toString() { + return this.hookName + } + toCode() { return this.toString() + '()' } retry(n) { - // must be implemented for each hook + this.suite.opts[`retry${this.hookName}`] = n } get title() { @@ -31,29 +35,13 @@ class Hook { } } -class BeforeHook extends Hook { - retry(n) { - this.suite.opts['retryBefore'] = n - } -} +class BeforeHook extends Hook {} -class AfterHook extends Hook { - retry(n) { - this.suite.opts['retryAfter'] = n - } -} +class AfterHook extends Hook {} -class BeforeSuiteHook extends Hook { - retry(n) { - this.suite.opts['retryBeforeSuite'] = n - } -} +class BeforeSuiteHook extends Hook {} -class AfterSuiteHook extends Hook { - retry(n) { - this.suite.opts['retryAfterSuite'] = n - } -} +class AfterSuiteHook extends Hook {} function fireHook(eventType, suite, error) { const hook = suite.ctx?.test?.title?.match(/"([^"]*)"/)[1] diff --git a/lib/mocha/test.js b/lib/mocha/test.js index 0ca7cb926..836f1f6cf 100644 --- a/lib/mocha/test.js +++ b/lib/mocha/test.js @@ -1,5 +1,5 @@ const Test = require('mocha/lib/test') -const scenario = require('./scenario') +const { test: testWrapper } = require('./asyncWrapper') const { enhanceMochaSuite } = require('./suite') /** @@ -37,7 +37,7 @@ function enhanceMochaTest(test) { */ test.addToSuite = function (suite) { enhanceMochaSuite(suite) - suite.addTest(scenario.test(this)) + suite.addTest(testWrapper(this)) test.tags = [...(test.tags || []), ...(suite.tags || [])] test.fullTitle = () => `${suite.title}: ${test.title}` } diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js index 93686701c..244ea73f2 100644 --- a/lib/mocha/ui.js +++ b/lib/mocha/ui.js @@ -1,5 +1,5 @@ const escapeRe = require('escape-string-regexp') -const scenario = require('./scenario') +const { test, setup, teardown, suiteSetup, suiteTeardown, injected } = require('./asyncWrapper') const ScenarioConfig = require('./scenarioConfig') const FeatureConfig = require('./featureConfig') const addDataContext = require('../data/context') @@ -94,11 +94,11 @@ module.exports = function (suite) { suite.file = file suites.unshift(suite) - suite.beforeEach('codeceptjs.before', () => scenario.setup(suite)) - afterEachHooks.push(['finalize codeceptjs', () => scenario.teardown(suite)]) + suite.beforeEach('codeceptjs.before', () => setup(suite)) + afterEachHooks.push(['finalize codeceptjs', () => teardown(suite)]) - suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite)) - afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]) + suite.beforeAll('codeceptjs.beforeSuite', () => suiteSetup(suite)) + afterAllHooks.push(['codeceptjs.afterSuite', () => suiteTeardown(suite)]) return new FeatureConfig(suite) } @@ -118,22 +118,22 @@ module.exports = function (suite) { } context.BeforeSuite = function (fn) { - suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite')) + suites[0].beforeAll('BeforeSuite', injected(fn, suites[0], 'beforeSuite')) return new HookConfig(new BeforeSuiteHook({ suite: suites[0] })) } context.AfterSuite = function (fn) { - afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]) + afterAllHooks.unshift(['AfterSuite', injected(fn, suites[0], 'afterSuite')]) return new HookConfig(new AfterSuiteHook({ suite: suites[0] })) } context.Background = context.Before = function (fn) { - suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before')) + suites[0].beforeEach('Before', injected(fn, suites[0], 'before')) return new HookConfig(new BeforeHook({ suite: suites[0] })) } context.After = function (fn) { - afterEachHooks.unshift(['After', scenario.injected(fn, suites[0], 'after')]) + afterEachHooks.unshift(['After', injected(fn, suites[0], 'after')]) return new HookConfig(new AfterHook({ suite: suites[0] })) } diff --git a/test/data/sandbox/configs/retryHooks/codecept.retry.hookconfig.conf.js b/test/data/sandbox/configs/retryHooks/codecept.retry.hookconfig.conf.js new file mode 100644 index 000000000..cb416ce47 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.retry.hookconfig.conf.js @@ -0,0 +1,12 @@ +exports.config = { + tests: './*_test2.js', + output: './output', + helpers: { + CustomHelper: { + require: './helper.js', + }, + }, + bootstrap: null, + mocha: {}, + name: 'retryHooks', +} diff --git a/test/data/sandbox/configs/retryHooks/retry_async_hook_test2.js b/test/data/sandbox/configs/retryHooks/retry_async_hook_test2.js new file mode 100644 index 000000000..af49142b4 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_async_hook_test2.js @@ -0,0 +1,18 @@ +Feature('Retry #Async hooks') + +let i = 0 + +Before(async ({ I }) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + console.log('ok', i, new Date()) + i++ + if (i < 3) reject(new Error('not works')) + resolve() + }, 0) + }) +}).retry(2) + +Scenario('async hook works', () => { + console.log('works') +}) diff --git a/test/runner/retry_hooks_test.js b/test/runner/retry_hooks_test.js index 8acf2afa7..f30610c4a 100644 --- a/test/runner/retry_hooks_test.js +++ b/test/runner/retry_hooks_test.js @@ -17,6 +17,14 @@ describe('CodeceptJS Retry Hooks', function () { }) }) }) + + it('run should load hook config from Before().retry()', done => { + exec(config_run_config('codecept.retry.hookconfig.conf.js', '#Async '), (err, stdout) => { + debug_this_test && console.log(stdout) + expect(stdout).toContain('1 passed') + done() + }) + }) ;['#Before ', '#BeforeSuite '].forEach(retryHook => { it(`should ${retryHook} set hook retries from global config`, done => { exec(config_run_config('codecept.retry.obj.conf.js', retryHook), (err, stdout) => { diff --git a/test/unit/scenario_test.js b/test/unit/mocha/asyncWrapper_test.js similarity index 79% rename from test/unit/scenario_test.js rename to test/unit/mocha/asyncWrapper_test.js index 601956989..284e01641 100644 --- a/test/unit/scenario_test.js +++ b/test/unit/mocha/asyncWrapper_test.js @@ -4,9 +4,9 @@ import('chai').then(chai => { }) const sinon = require('sinon') -const scenario = require('../../lib/mocha/scenario') -const recorder = require('../../lib/recorder') -const event = require('../../lib/event') +const { test: testWrapper, setup, teardown, suiteSetup, suiteTeardown } = require('../../../lib/mocha/asyncWrapper') +const recorder = require('../../../lib/recorder') +const event = require('../../../lib/event') let test let fn @@ -17,7 +17,7 @@ let afterSuite let failed let started -describe('Scenario', () => { +describe('AsyncWrapper', () => { beforeEach(() => { test = { timeout: () => {} } fn = sinon.spy() @@ -27,7 +27,7 @@ describe('Scenario', () => { afterEach(() => event.cleanDispatcher()) it('should wrap test function', () => { - scenario.test(test).fn(() => {}) + testWrapper(test).fn(() => {}) expect(fn.called).is.ok }) @@ -42,8 +42,8 @@ describe('Scenario', () => { }) } - scenario.setup() - scenario.test(test).fn(() => null) + setup() + testWrapper(test).fn(() => null) recorder.add('validation', () => expect(counter).to.eq(4)) return recorder.promise() }) @@ -55,15 +55,15 @@ describe('Scenario', () => { event.dispatcher.on(event.test.started, (started = sinon.spy())) event.dispatcher.on(event.suite.before, (beforeSuite = sinon.spy())) event.dispatcher.on(event.suite.after, (afterSuite = sinon.spy())) - scenario.suiteSetup() - scenario.setup() + suiteSetup() + setup() }) it('should fire events', () => { - scenario.test(test).fn(() => null) + testWrapper(test).fn(() => null) expect(started.called).is.ok - scenario.teardown() - scenario.suiteTeardown() + teardown() + suiteTeardown() return recorder .promise() .then(() => expect(beforeSuite.called).is.ok) @@ -74,11 +74,11 @@ describe('Scenario', () => { it('should fire failed event on error', () => { event.dispatcher.on(event.test.failed, (failed = sinon.spy())) - scenario.setup() + setup() test.fn = () => { throw new Error('ups') } - scenario.test(test).fn(() => {}) + testWrapper(test).fn(() => {}) return recorder .promise() .then(() => expect(failed.called).is.ok) @@ -89,7 +89,7 @@ describe('Scenario', () => { test.fn = () => { recorder.throw(new Error('ups')) } - scenario.test(test).fn(() => {}) + testWrapper(test).fn(() => {}) return recorder .promise() .then(() => expect(failed.called).is.ok) diff --git a/test/unit/ui_test.js b/test/unit/mocha/ui_test.js similarity index 98% rename from test/unit/ui_test.js rename to test/unit/mocha/ui_test.js index bcbb5a03c..6ad5f3afa 100644 --- a/test/unit/ui_test.js +++ b/test/unit/mocha/ui_test.js @@ -5,9 +5,9 @@ import('chai').then(chai => { const Mocha = require('mocha/lib/mocha') const Suite = require('mocha/lib/suite') -global.codeceptjs = require('../../lib') -const makeUI = require('../../lib/mocha/ui') -const container = require('../../lib/container') +global.codeceptjs = require('../../../lib') +const makeUI = require('../../../lib/mocha/ui') +const container = require('../../../lib/container') describe('ui', () => { let suite