diff --git a/lib/listener/steps.js b/lib/listener/steps.js index aa33d1e14..85750f278 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -2,21 +2,15 @@ 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 /** - * 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.started, test => { + test.startedAt = +new Date() currentTest = test currentTest.steps = [] if (!('retryNum' in currentTest)) currentTest.retryNum = 0 @@ -36,13 +30,13 @@ module.exports = function () { output.hook.started(hook) - if (hook.ctx && hook.ctx.test) debug(`--- STARTED ${hook.ctx.test.title} ---`) + if (hook.ctx && hook.ctx.test) debug(`--- STARTED ${hook.title} ---`) }) event.dispatcher.on(event.hook.passed, hook => { currentHook = null output.hook.passed(hook) - if (hook.ctx && hook.ctx.test) debug(`--- ENDED ${hook.ctx.test.title} ---`) + if (hook.ctx && hook.ctx.test) debug(`--- ENDED ${hook.title} ---`) }) event.dispatcher.on(event.test.failed, () => { @@ -88,4 +82,61 @@ module.exports = function () { store.currentStep = null store.stepOptions = null }) + + // listeners to output steps + let currentMetaStep = [] + + event.dispatcher.on(event.bddStep.started, step => { + if (!printSteps()) return + + output.stepShift = 2 + output.step(step) + }) + + event.dispatcher.on(event.step.started, step => { + if (!printSteps()) return + + let processingStep = step + const metaSteps = [] + let isHidden = false + while (processingStep.metaStep) { + metaSteps.unshift(processingStep.metaStep) + processingStep = processingStep.metaStep + if (processingStep.collapsed) isHidden = true + } + 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 + + if (isHidden) return + output.stepShift = 3 + 2 * shift + output.step(step) + }) + + event.dispatcher.on(event.step.finished, () => { + if (!printSteps()) return + output.stepShift = 0 + }) +} + +let areStepsPrinted = false +function printSteps() { + if (output.level() < 1) return false + + // if executed first time, print debug message + if (!areStepsPrinted) { + debug('Printing steps', 'Output level', output.level()) + areStepsPrinted = true + } + + return true } diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js index 64fd72d49..e9a0ffa32 100644 --- a/lib/mocha/cli.js +++ b/lib/mocha/cli.js @@ -79,44 +79,6 @@ class Cli extends Base { 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 => { diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index 3c9aa7d4d..40b3b46e0 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -7,6 +7,7 @@ class Hook { this.runnable = context?.ctx?.test this.ctx = context.ctx this.error = error + this.steps = [] } get hookName() { diff --git a/lib/plugin/commentStep.js b/lib/plugin/commentStep.js index dad519e20..b368f3ca1 100644 --- a/lib/plugin/commentStep.js +++ b/lib/plugin/commentStep.js @@ -7,6 +7,8 @@ let currentCommentStep const defaultGlobalName = '__' /** + * @deprecated + * * Add descriptive nested steps for your tests: * * ```js @@ -100,6 +102,9 @@ const defaultGlobalName = '__' * ``` */ module.exports = function (config) { + console.log('commentStep is deprecated, disable it and use Section instead') + console.log('const { Section: __ } = require("codeceptjs/steps")') + event.dispatcher.on(event.test.started, () => { currentCommentStep = null }) diff --git a/lib/step/base.js b/lib/step/base.js index 47bbe4f5c..94afc675e 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -3,7 +3,7 @@ const Secret = require('../secret') const { getCurrentTimeout } = require('./timeout') const { ucfirst, humanizeString } = require('../utils') -const STACK_LINE = 4 +const STACK_LINE = 5 /** * Each command in test executed through `I.` object is wrapped in Step. @@ -166,7 +166,7 @@ class Step { processingStep = this while (processingStep.metaStep) { - if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) { + if (processingStep.metaStep.actor?.match(/^(Given|When|Then|And)/)) { hasBDD = true break } else { diff --git a/lib/step/meta.js b/lib/step/meta.js index bc2b0a39e..c4acd8e80 100644 --- a/lib/step/meta.js +++ b/lib/step/meta.js @@ -6,6 +6,10 @@ class MetaStep extends Step { constructor(actor, method) { if (!method) method = '' super(method) + + /** @member {boolean} collsapsed hide children steps from output */ + this.collapsed = false + this.actor = actor } @@ -32,7 +36,11 @@ class MetaStep extends Step { return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` } - return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` + if (!this.actor) { + return `${this.name} ${this.humanizeArgs()}${this.suffix}`.trim() + } + + return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`.trim() } humanize() { diff --git a/lib/step/section.js b/lib/step/section.js new file mode 100644 index 000000000..ac93852b9 --- /dev/null +++ b/lib/step/section.js @@ -0,0 +1,55 @@ +const MetaStep = require('./meta') +const event = require('../event') + +let currentSection + +class Section { + constructor(name = '') { + this.name = name + + this.metaStep = new MetaStep(null, name) + + this.attachMetaStep = step => { + if (currentSection !== this) return + if (!step) return + const metaStep = getRootMetaStep(step) + + if (metaStep !== this.metaStep) { + metaStep.metaStep = this.metaStep + } + } + } + + hidden() { + this.metaStep.collapsed = true + return this + } + + start() { + if (currentSection) currentSection.end() + currentSection = this + event.dispatcher.prependListener(event.step.before, this.attachMetaStep) + event.dispatcher.once(event.test.finished, () => this.end()) + return this + } + + end() { + currentSection = null + event.dispatcher.off(event.step.started, this.attachMetaStep) + return this + } + + /** + * @returns {Section} + */ + static current() { + return currentSection + } +} + +function getRootMetaStep(step) { + if (step.metaStep) return getRootMetaStep(step.metaStep) + return step +} + +module.exports = Section diff --git a/lib/steps.js b/lib/steps.js index fc126629d..564608925 100644 --- a/lib/steps.js +++ b/lib/steps.js @@ -1,5 +1,5 @@ const StepConfig = require('./step/config') - +const Section = require('./step/section') function stepOpts(opts = {}) { return new StepConfig(opts) } @@ -12,12 +12,39 @@ function stepRetry(retry) { return new StepConfig().retry(retry) } +function section(name) { + if (!name) return endSection() + return new Section(name).start() +} + +function endSection() { + return Section.current().end() +} + // Section function to be added here const step = { + // steps.opts syntax opts: stepOpts, timeout: stepTimeout, retry: stepRetry, + + // one-function syntax + stepTimeout, + stepRetry, + stepOpts, + + // sections + section, + endSection, + + Section: section, + EndSection: endSection, + + // shortcuts + Given: () => section('Given'), + When: () => section('When'), + Then: () => section('Then'), } module.exports = step diff --git a/runok.js b/runok.js index 8bfa171db..56812e3cd 100755 --- a/runok.js +++ b/runok.js @@ -607,7 +607,7 @@ describe('CodeceptJS ${featureName}', function () { console.log(`Created test files for feature: ${featureName}`) console.log('Run codecept tests with:') - console.log(`./bin/codecept.js run --config ${configDir}/codecept.${featureName}.conf.js`) + console.log(`./bin/codecept.js run --config ${configDir}/codecept.conf.js`) console.log('') console.log('Run tests with:') diff --git a/test/data/sandbox/configs/step-sections/codecept.conf.js b/test/data/sandbox/configs/step-sections/codecept.conf.js new file mode 100644 index 000000000..b30dc046a --- /dev/null +++ b/test/data/sandbox/configs/step-sections/codecept.conf.js @@ -0,0 +1,16 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + CustomHelper: { + require: './customHelper.js', + }, + }, + include: { + userPage: './userPage.js', + }, + bootstrap: false, + mocha: {}, + name: 'step-sections tests', +} diff --git a/test/data/sandbox/configs/step-sections/customHelper.js b/test/data/sandbox/configs/step-sections/customHelper.js new file mode 100644 index 000000000..2e435c3f4 --- /dev/null +++ b/test/data/sandbox/configs/step-sections/customHelper.js @@ -0,0 +1,7 @@ +class CustomHelper extends Helper { + act() { + this.debug(JSON.stringify(arguments)) + } +} + +module.exports = CustomHelper diff --git a/test/data/sandbox/configs/step-sections/step-sections_test.js b/test/data/sandbox/configs/step-sections/step-sections_test.js new file mode 100644 index 000000000..d28a09db9 --- /dev/null +++ b/test/data/sandbox/configs/step-sections/step-sections_test.js @@ -0,0 +1,34 @@ +const { Section, EndSection } = require('codeceptjs/steps') + +Feature('step-sections') + +Scenario('test using of basic step-sections', ({ I }) => { + I.amInPath('.') + + Section('User Journey') + I.act('Hello, World!') + + Section() + I.act('Nothing to say') +}) + +Scenario('test using of step-sections and page objects', ({ I, userPage }) => { + Section('User Journey') + userPage.actOnPage() + + I.act('One more step') + + Section() + + I.act('Nothing to say') +}) + +Scenario('test using of hidden step-sections', ({ I, userPage }) => { + Section('User Journey').hidden() + userPage.actOnPage() + I.act('One more step') + + EndSection() + + I.act('Nothing to say') +}) diff --git a/test/data/sandbox/configs/step-sections/userPage.js b/test/data/sandbox/configs/step-sections/userPage.js new file mode 100644 index 000000000..505b56b48 --- /dev/null +++ b/test/data/sandbox/configs/step-sections/userPage.js @@ -0,0 +1,8 @@ +const { I } = inject() + +module.exports = { + actOnPage: () => { + I.act('actOnPage') + I.act('see on this page') + }, +} diff --git a/test/runner/step-sections_test.js b/test/runner/step-sections_test.js new file mode 100644 index 000000000..340163c62 --- /dev/null +++ b/test/runner/step-sections_test.js @@ -0,0 +1,52 @@ +const { expect } = require('expect') +const exec = require('child_process').exec +const { codecept_dir, codecept_run } = require('./consts') +const debug = require('debug')('codeceptjs:tests') + +const config_run_config = (config, grep) => `${codecept_run} --steps --config ${codecept_dir}/configs/step-sections/${config} ${grep ? `--grep "${grep}"` : ''}` + +describe('CodeceptJS step-sections', function () { + this.timeout(10000) + + it('should run step-sections test', done => { + exec(config_run_config('codecept.conf.js', 'basic step-sections'), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(stdout).toContain('User Journey') + expect(stdout).toContain('Nothing to say') + + const expectedOutput = [' I am in path "."', ' User Journey', ' I act "Hello, World!"', ' I act "Nothing to say"'].join('\n') + + expect(stdout).toContain(expectedOutput) + expect(err).toBeFalsy() + done() + }) + }) + + it('should run step-sections with page objects', done => { + exec(config_run_config('codecept.conf.js', 'sections and page objects'), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(stdout).toContain('User Journey') + + const expectedOutput = [' User Journey', ' On userPage: act on page', ' I act "actOnPage"', ' I act "see on this page"', ' I act "One more step"', ' I act "Nothing to say"'].join('\n') + + expect(stdout).toContain(expectedOutput) + expect(err).toBeFalsy() + done() + }) + }) + + it('should run hidden step-sections', done => { + exec(config_run_config('codecept.conf.js', 'hidden step-sections'), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + + expect(stdout).toContain('User Journey') + expect(stdout).not.toContain('actOnPage') + expect(stdout).not.toContain('One more step') + expect(err).toBeFalsy() + done() + }) + }) +})