From 99d93f9f280877fa0055412741ea33ddb13d05b0 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 10 Jan 2025 06:20:19 +0200 Subject: [PATCH 01/12] initial implementation for step params --- lib/effects.js | 9 +++++++++ lib/helper/Playwright.js | 6 ++++++ lib/listener/steps.js | 3 +++ lib/step.js | 16 ++++++++++++++++ lib/store.js | 2 ++ package.json | 3 ++- 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 lib/effects.js diff --git a/lib/effects.js b/lib/effects.js new file mode 100644 index 000000000..a16341986 --- /dev/null +++ b/lib/effects.js @@ -0,0 +1,9 @@ +const { StepConfig } = require('./step') + +function stepConfig(opts) { + return new StepConfig(opts) +} + +module.exports = { + stepConfig, +} diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index f074c710f..d6cb11459 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -3499,6 +3499,12 @@ async function proceedSee(assertType, text, context, strict = false) { allText = await Promise.all(els.map(el => el.innerText())) } + if (store?.currentStep?.opts?.ignoreCase === true) { + description = description.toLowerCase() + text = text.toLowerCase() + allText = allText.map(elText => elText.toLowerCase()) + } + if (strict) { return allText.map(elText => equals(description)[assertType](text, elText)) } diff --git a/lib/listener/steps.js b/lib/listener/steps.js index 38d166c0b..aa33d1e14 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -73,6 +73,7 @@ module.exports = function () { event.dispatcher.on(event.step.started, step => { step.startedAt = +new Date() step.test = currentTest + store.currentStep = step if (currentHook && Array.isArray(currentHook.steps)) { return currentHook.steps.push(step) } @@ -84,5 +85,7 @@ module.exports = function () { step.finishedAt = +new Date() if (step.startedAt) step.duration = step.finishedAt - step.startedAt debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`) + store.currentStep = null + store.stepOptions = null }) } diff --git a/lib/step.js b/lib/step.js index 51f1ff49b..e8d94641f 100644 --- a/lib/step.js +++ b/lib/step.js @@ -61,6 +61,8 @@ class Step { this.metaStep = undefined /** @member {string} */ this.stack = '' + /** @member {StepConfig} */ + this.opts = {} const timeouts = new Map() /** @@ -114,6 +116,13 @@ class Step { */ run() { this.args = Array.prototype.slice.call(arguments) + const lastArg = this.args[this.args.length - 1] + if (lastArg instanceof StepConfig) { + const config = this.args.pop() + store.stepOptions = config.options + this.opts = config.options + } + if (store.dryRun) { this.setStatus('success') return Promise.resolve(new Proxy({}, dryRunResolver())) @@ -321,10 +330,17 @@ class MetaStep extends Step { } } +class StepConfig { + constructor(opts) { + this.options = opts + } +} + Step.TIMEOUTS = {} /** @type {Class} */ Step.MetaStep = MetaStep +Step.StepConfig = StepConfig module.exports = Step diff --git a/lib/store.js b/lib/store.js index 9a29577d6..352b2d27c 100644 --- a/lib/store.js +++ b/lib/store.js @@ -13,6 +13,8 @@ const store = { onPause: false, /** @type {CodeceptJS.Test | null} */ currentTest: null, + /** @type {any} */ + currentStep: null, } module.exports = store diff --git a/package.json b/package.json index b661b3f32..44f16c6af 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "main": "lib/index.js", "exports": { ".": "./lib/index.js", - "./els": "./lib/els.js" + "./els": "./lib/els.js", + "./effects": "./lib/effects.js" }, "types": "typings/index.d.ts", "bin": { From d9998f35e9114ab89cd88cc3173e3211a4cdae13 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 12 Jan 2025 23:20:23 +0200 Subject: [PATCH 02/12] refactored steps --- lib/actor.js | 136 ++++++---------- lib/effects.js | 9 -- lib/recorder.js | 2 +- lib/step.js | 369 +------------------------------------------- lib/step/base.js | 190 +++++++++++++++++++++++ lib/step/config.js | 30 ++++ lib/step/helper.js | 47 ++++++ lib/step/meta.js | 91 +++++++++++ lib/step/record.js | 76 +++++++++ lib/step/timeout.js | 42 +++++ lib/steps.js | 23 +++ lib/utils.js | 11 ++ lib/within.js | 4 +- 13 files changed, 559 insertions(+), 471 deletions(-) create mode 100644 lib/step/base.js create mode 100644 lib/step/config.js create mode 100644 lib/step/helper.js create mode 100644 lib/step/meta.js create mode 100644 lib/step/record.js create mode 100644 lib/step/timeout.js create mode 100644 lib/steps.js diff --git a/lib/actor.js b/lib/actor.js index 63b5067fe..15e0fe3f5 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -1,11 +1,13 @@ -const Step = require('./step'); -const { MetaStep } = require('./step'); -const container = require('./container'); -const { methodsOfObject } = require('./utils'); -const recorder = require('./recorder'); -const event = require('./event'); -const store = require('./store'); -const output = require('./output'); +const Step = require('./step') +const MetaStep = require('./step/meta') +const recordStep = require('./step/record') +const container = require('./container') +const { methodsOfObject } = require('./utils') +const { TIMEOUT_ORDER } = require('./step/timeout') +const recorder = require('./recorder') +const event = require('./event') +const store = require('./store') +const output = require('./output') /** * @interface @@ -21,13 +23,13 @@ class Actor { * ⚠️ returns a promise which is synchronized internally by recorder */ async say(msg, color = 'cyan') { - const step = new Step('say', 'say'); - step.status = 'passed'; + const step = new Step('say', 'say') + step.status = 'passed' return recordStep(step, [msg]).then(() => { // this is backward compatibility as this event may be used somewhere - event.emit(event.step.comment, msg); - output.say(msg, `${color}`); - }); + event.emit(event.step.comment, msg) + output.say(msg, `${color}`) + }) } /** @@ -38,14 +40,14 @@ class Actor { * @inner */ limitTime(timeout) { - if (!store.timeouts) return this; + if (!store.timeouts) return this event.dispatcher.prependOnceListener(event.step.before, step => { - output.log(`Timeout to ${step}: ${timeout}s`); - step.setTimeout(timeout * 1000, Step.TIMEOUT_ORDER.codeLimitTime); - }); + output.log(`Timeout to ${step}: ${timeout}s`) + step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime) + }) - return this; + return this } /** @@ -55,11 +57,9 @@ class Actor { * @inner */ retry(opts) { - if (opts === undefined) opts = 1; - recorder.retry(opts); - // remove retry once the step passed - recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())); - return this; + const retryStep = require('./step/retry') + retryStep(opts) + return this } } @@ -70,102 +70,54 @@ class Actor { * @ignore */ module.exports = function (obj = {}) { - const actor = container.actor() || new Actor(); + const actor = container.actor() || new Actor() // load all helpers once container initialized container.started(() => { - const translation = container.translation(); - const helpers = container.helpers(); + const translation = container.translation() + const helpers = container.helpers() // add methods from enabled helpers Object.values(helpers).forEach(helper => { methodsOfObject(helper, 'Helper') .filter(method => method !== 'constructor' && method[0] !== '_') .forEach(action => { - const actionAlias = translation.actionAliasFor(action); + const actionAlias = translation.actionAliasFor(action) if (!actor[action]) { actor[action] = actor[actionAlias] = function () { - const step = new Step(helper, action); + const step = new Step(helper, action) if (translation.loaded) { - step.name = actionAlias; - step.actor = translation.I; + step.name = actionAlias + step.actor = translation.I } // add methods to promise chain - return recordStep(step, Array.from(arguments)); - }; + return recordStep(step, Array.from(arguments)) + } } - }); - }); + }) + }) // add translated custom steps from actor Object.keys(obj).forEach(key => { - const actionAlias = translation.actionAliasFor(key); + const actionAlias = translation.actionAliasFor(key) if (!actor[actionAlias]) { - actor[actionAlias] = actor[key]; + actor[actionAlias] = actor[key] } - }); + }) container.append({ support: { I: actor, }, - }); - }); + }) + }) // store.actor = actor; // add custom steps from actor Object.keys(obj).forEach(key => { - const ms = new MetaStep('I', key); - ms.setContext(actor); - actor[key] = ms.run.bind(ms, obj[key]); - }); + const ms = new MetaStep('I', key) + ms.setContext(actor) + actor[key] = ms.run.bind(ms, obj[key]) + }) - return actor; -}; - -function recordStep(step, args) { - step.status = 'queued'; - step.setArguments(args); - - // run async before step hooks - event.emit(event.step.before, step); - - const task = `${step.name}: ${step.humanizeArgs()}`; - let val; - - // run step inside promise - recorder.add( - task, - () => { - if (!step.startTime) { - // step can be retries - event.emit(event.step.started, step); - step.startTime = Date.now(); - } - return (val = step.run(...args)); - }, - false, - undefined, - step.getTimeout(), - ); - - event.emit(event.step.after, step); - - recorder.add('step passed', () => { - step.endTime = Date.now(); - event.emit(event.step.passed, step, val); - event.emit(event.step.finished, step); - }); - - recorder.catchWithoutStop(err => { - step.status = 'failed'; - step.endTime = Date.now(); - event.emit(event.step.failed, step); - event.emit(event.step.finished, step); - throw err; - }); - - recorder.add('return result', () => val); - // run async after step hooks - - return recorder.promise(); + return actor } diff --git a/lib/effects.js b/lib/effects.js index d3be77583..f5b8890cb 100644 --- a/lib/effects.js +++ b/lib/effects.js @@ -1,16 +1,7 @@ const recorder = require('./recorder') const { debug } = require('./output') -const { StepConfig } = require('./step') const store = require('./store') -function stepConfig(opts) { - return new StepConfig(opts) -} - -module.exports = { - stepConfig, -} - /** * @module hopeThat * diff --git a/lib/recorder.js b/lib/recorder.js index 40db146c7..fa60727d5 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -179,7 +179,7 @@ module.exports = { } if (retry === undefined) retry = true if (!running && !force) { - return + return Promise.resolve() } tasks.push(taskName) debug(chalk.gray(`${currentQueue()} Queued | ${taskName}`)) diff --git a/lib/step.js b/lib/step.js index e8d94641f..2a27ab4a7 100644 --- a/lib/step.js +++ b/lib/step.js @@ -1,369 +1,4 @@ -// TODO: place MetaStep in other file, disable rule - -const color = require('chalk') -const store = require('./store') -const Secret = require('./secret') -const event = require('./event') -const { ucfirst } = require('./utils') - -const STACK_LINE = 4 - -/** - * Each command in test executed through `I.` object is wrapped in Step. - * Step allows logging executed commands and triggers hook before and after step execution. - * @param {CodeceptJS.Helper} helper - * @param {string} name - */ -class Step { - static get TIMEOUT_ORDER() { - return { - /** - * timeouts set with order below zero only override timeouts of higher order if their value is smaller - */ - testOrSuite: -5, - /** - * 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true - */ - stepTimeoutHard: 5, - /** - * 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation - */ - codeLimitTime: 15, - /** - * 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false - */ - stepTimeoutSoft: 25, - } - } - - constructor(helper, name) { - /** @member {string} */ - this.actor = 'I' // I = actor - /** @member {CodeceptJS.Helper} */ - this.helper = helper // corresponding helper - /** @member {string} */ - this.name = name // name of a step console - /** @member {string} */ - this.helperMethod = name // helper method - /** @member {string} */ - this.status = 'pending' - /** - * @member {string} suffix - * @memberof CodeceptJS.Step# - */ - /** @member {string} */ - this.prefix = this.suffix = '' - /** @member {string} */ - this.comment = '' - /** @member {Array<*>} */ - this.args = [] - /** @member {MetaStep} */ - this.metaStep = undefined - /** @member {string} */ - this.stack = '' - /** @member {StepConfig} */ - this.opts = {} - - const timeouts = new Map() - /** - * @method - * @returns {number|undefined} - */ - this.getTimeout = function () { - let totalTimeout - // iterate over all timeouts starting from highest values of order - new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => { - if ( - timeout !== undefined && - // when orders >= 0 - timeout value overrides those set with higher order elements - (order >= 0 || - // when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order - totalTimeout === undefined || - // when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order - (timeout > 0 && (timeout < totalTimeout || totalTimeout === 0))) - ) { - totalTimeout = timeout - } - }) - return totalTimeout - } - /** - * @method - * @param {number} timeout - timeout in milliseconds or 0 if no timeout - * @param {number} order - order defines the priority of timeout, timeouts set with lower order override those set with higher order. - * When order below 0 value of timeout only override if new value is lower - */ - this.setTimeout = function (timeout, order) { - timeouts.set(order, timeout) - } - - this.setTrace() - } - - /** @function */ - setTrace() { - Error.captureStackTrace(this) - } - - /** @param {Array<*>} args */ - setArguments(args) { - this.args = args - } - - /** - * @param {...any} args - * @return {*} - */ - run() { - this.args = Array.prototype.slice.call(arguments) - const lastArg = this.args[this.args.length - 1] - if (lastArg instanceof StepConfig) { - const config = this.args.pop() - store.stepOptions = config.options - this.opts = config.options - } - - if (store.dryRun) { - this.setStatus('success') - return Promise.resolve(new Proxy({}, dryRunResolver())) - } - let result - try { - if (this.helperMethod !== 'say') { - result = this.helper[this.helperMethod].apply(this.helper, this.args) - } - this.setStatus('success') - } catch (err) { - this.setStatus('failed') - throw err - } - return result - } - - setActor(actor) { - this.actor = actor || '' - } - - /** @param {string} status */ - setStatus(status) { - this.status = status - if (this.metaStep) { - this.metaStep.setStatus(status) - } - } - - /** @return {string} */ - humanize() { - return humanizeString(this.name) - } - - /** @return {string} */ - humanizeArgs() { - return this.args - .map(arg => { - if (!arg) { - return '' - } - if (typeof arg === 'string') { - return `"${arg}"` - } - if (Array.isArray(arg)) { - try { - const res = JSON.stringify(arg) - return res - } catch (err) { - return `[${arg.toString()}]` - } - } else if (typeof arg === 'function') { - return arg.toString() - } else if (typeof arg === 'undefined') { - return `${arg}` - } else if (arg instanceof Secret) { - return arg.getMasked() - } else if (arg.toString && arg.toString() !== '[object Object]') { - return arg.toString() - } else if (typeof arg === 'object') { - const returnedArg = {} - for (const [key, value] of Object.entries(arg)) { - returnedArg[key] = value - if (value instanceof Secret) returnedArg[key] = value.getMasked() - } - return JSON.stringify(returnedArg) - } - return arg - }) - .join(', ') - } - - /** @return {string} */ - line() { - const lines = this.stack.split('\n') - if (lines[STACK_LINE]) { - return lines[STACK_LINE].trim() - .replace(global.codecept_dir || '', '.') - .trim() - } - return '' - } - - /** @return {string} */ - toString() { - return ucfirst(`${this.prefix}${this.actor} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`).trim() - } - - /** @return {string} */ - toCliStyled() { - return `${this.prefix}${this.actor} ${color.italic(this.humanize())} ${color.yellow(this.humanizeArgs())}${this.suffix}` - } - - /** @return {string} */ - toCode() { - return `${this.prefix}${this.actor}.${this.name}(${this.humanizeArgs()})${this.suffix}` - } - - isMetaStep() { - return this.constructor.name === 'MetaStep' - } - - /** @return {boolean} */ - hasBDDAncestor() { - let hasBDD = false - let processingStep - processingStep = this - - while (processingStep.metaStep) { - if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) { - hasBDD = true - break - } else { - processingStep = processingStep.metaStep - } - } - return hasBDD - } -} - -/** @extends Step */ -class MetaStep extends Step { - constructor(obj, method) { - if (!method) method = '' - super(null, method) - this.actor = obj - } - - /** @return {boolean} */ - isBDD() { - if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) { - return true - } - return false - } - - toCliStyled() { - return this.toString() - } - - toString() { - const actorText = this.actor - - if (this.isBDD()) { - return `${this.prefix}${actorText} ${this.name} "${this.humanizeArgs()}${this.suffix}"` - } - - if (actorText === 'I') { - return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` - } - - return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` - } - - humanize() { - return humanizeString(this.name) - } - - setTrace() {} - - setContext(context) { - this.context = context - } - - /** @return {*} */ - run(fn) { - this.status = 'queued' - this.setArguments(Array.from(arguments).slice(1)) - let result - - const registerStep = step => { - this.metaStep = null - step.metaStep = this - } - event.dispatcher.prependListener(event.step.before, registerStep) - // Handle async and sync methods. - if (fn.constructor.name === 'AsyncFunction') { - result = fn - .apply(this.context, this.args) - .then(result => { - return result - }) - .catch(error => { - this.setStatus('failed') - throw error - }) - .finally(() => { - this.endTime = Date.now() - event.dispatcher.removeListener(event.step.before, registerStep) - }) - } else { - try { - this.startTime = Date.now() - result = fn.apply(this.context, this.args) - } catch (error) { - this.setStatus('failed') - throw error - } finally { - this.endTime = Date.now() - event.dispatcher.removeListener(event.step.before, registerStep) - } - } - - return result - } -} - -class StepConfig { - constructor(opts) { - this.options = opts - } -} - -Step.TIMEOUTS = {} - -/** @type {Class} */ -Step.MetaStep = MetaStep -Step.StepConfig = StepConfig +// refactored step class, moved to helper +const Step = require('./step/helper') module.exports = Step - -function dryRunResolver() { - return { - get(target, prop) { - if (prop === 'toString') return () => '' - return new Proxy({}, dryRunResolver()) - }, - } -} - -function humanizeString(string) { - // split strings by words, then make them all lowercase - const _result = string - .replace(/([a-z](?=[A-Z]))/g, '$1 ') - .split(' ') - .map(word => word.toLowerCase()) - - _result[0] = _result[0] === 'i' ? capitalizeFLetter(_result[0]) : _result[0] - return _result.join(' ').trim() -} - -function capitalizeFLetter(string) { - return string[0].toUpperCase() + string.slice(1) -} diff --git a/lib/step/base.js b/lib/step/base.js new file mode 100644 index 000000000..f3303b606 --- /dev/null +++ b/lib/step/base.js @@ -0,0 +1,190 @@ +const color = require('chalk') +const Secret = require('../secret') +const { getCurrentTimeout } = require('./timeout') +const { ucfirst, humanizeString } = require('../utils') + +const STACK_LINE = 4 + +/** + * Each command in test executed through `I.` object is wrapped in Step. + * Step allows logging executed commands and triggers hook before and after step execution. + * @param {CodeceptJS.Helper} helper + * @param {string} name + */ +class Step { + constructor(name) { + /** @member {string} */ + this.name = name + /** @member {Map} */ + this.timeouts = new Map() + + /** @member {Array<*>} */ + this.args = [] + + /** @member {StepConfig} */ + this.opts = {} + /** @member {string} */ + this.actor = 'I' // I = actor + /** @member {string} */ + this.helperMethod = name // helper method + /** @member {string} */ + this.status = 'pending' + /** @member {string} */ + this.prefix = this.suffix = '' + /** @member {string} */ + this.comment = '' + /** @member {MetaStep} */ + this.metaStep = undefined + /** @member {string} */ + this.stack = '' + + this.setTrace() + } + + setMetaStep(metaStep) { + this.metaStep = metaStep + } + + run() { + throw new Error('Not implemented') + } + + /** + * @returns {number|undefined} + */ + get timeout() { + return getCurrentTimeout(this.timeouts) + } + + /** + * @param {number} timeout - timeout in milliseconds or 0 if no timeout + * @param {number} order - order defines the priority of timeout, timeouts set with lower order override those set with higher order. + * When order below 0 value of timeout only override if new value is lower + */ + setTimeout(timeout, order) { + this.timeouts.set(order, timeout) + } + + /** @function */ + setTrace() { + Error.captureStackTrace(this) + } + + /** @param {Array<*>} args */ + setArguments(args) { + this.args = args + } + + setActor(actor) { + this.actor = actor || '' + } + + /** @param {string} status */ + setStatus(status) { + this.status = status + if (this.metaStep) { + this.metaStep.setStatus(status) + } + } + + /** @return {string} */ + humanize() { + return humanizeString(this.name) + } + + /** @return {string} */ + humanizeArgs() { + return this.args + .map(arg => { + if (!arg) { + return '' + } + if (typeof arg === 'string') { + return `"${arg}"` + } + if (Array.isArray(arg)) { + try { + const res = JSON.stringify(arg) + return res + } catch (err) { + return `[${arg.toString()}]` + } + } else if (typeof arg === 'function') { + return arg.toString() + } else if (typeof arg === 'undefined') { + return `${arg}` + } else if (arg instanceof Secret) { + return arg.getMasked() + } else if (arg.toString && arg.toString() !== '[object Object]') { + return arg.toString() + } else if (typeof arg === 'object') { + const returnedArg = {} + for (const [key, value] of Object.entries(arg)) { + returnedArg[key] = value + if (value instanceof Secret) returnedArg[key] = value.getMasked() + } + return JSON.stringify(returnedArg) + } + return arg + }) + .join(', ') + } + + /** @return {string} */ + line() { + const lines = this.stack.split('\n') + if (lines[STACK_LINE]) { + return lines[STACK_LINE].trim() + .replace(global.codecept_dir || '', '.') + .trim() + } + return '' + } + + /** @return {string} */ + toString() { + return ucfirst(`${this.prefix}${this.actor} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`).trim() + } + + /** @return {string} */ + toCliStyled() { + return `${this.prefix}${this.actor} ${color.italic(this.humanize())} ${color.yellow(this.humanizeArgs())}${this.suffix}` + } + + /** @return {string} */ + toCode() { + return `${this.prefix}${this.actor}.${this.name}(${this.humanizeArgs()})${this.suffix}` + } + + isMetaStep() { + return this.constructor.name === 'MetaStep' + } + + /** @return {boolean} */ + hasBDDAncestor() { + let hasBDD = false + let processingStep + processingStep = this + + while (processingStep.metaStep) { + if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) { + hasBDD = true + break + } else { + processingStep = processingStep.metaStep + } + } + return hasBDD + } +} + +module.exports = Step + +function dryRunResolver() { + return { + get(target, prop) { + if (prop === 'toString') return () => '' + return new Proxy({}, dryRunResolver()) + }, + } +} diff --git a/lib/step/config.js b/lib/step/config.js new file mode 100644 index 000000000..f4e6aa58d --- /dev/null +++ b/lib/step/config.js @@ -0,0 +1,30 @@ +class StepConfig { + constructor(opts) { + this.config = { + opts, + timeout: undefined, + retry: undefined, + } + } + + opts(opts) { + this.config.opts = opts + return this + } + + timeout(timeout) { + this.config.timeout = timeout + return this + } + + retry(retry) { + this.config.retry = retry + return this + } + + getConfig() { + return this.config + } +} + +module.exports = StepConfig diff --git a/lib/step/helper.js b/lib/step/helper.js new file mode 100644 index 000000000..b52470e3c --- /dev/null +++ b/lib/step/helper.js @@ -0,0 +1,47 @@ +const Step = require('./base') +const store = require('../store') + +class HelperStep extends Step { + constructor(helper, name) { + super(name) + /** @member {CodeceptJS.Helper} helper corresponding helper */ + this.helper = helper + /** @member {string} helperMethod name of method to be executed */ + this.helperMethod = name + } + + /** + * @param {...any} args + * @return {*} + */ + run() { + this.args = Array.prototype.slice.call(arguments) + + if (store.dryRun) { + this.setStatus('success') + return Promise.resolve(new Proxy({}, dryRunResolver())) + } + let result + try { + if (this.helperMethod !== 'say') { + result = this.helper[this.helperMethod].apply(this.helper, this.args) + } + this.setStatus('success') + } catch (err) { + this.setStatus('failed') + throw err + } + return result + } +} + +module.exports = HelperStep + +function dryRunResolver() { + return { + get(target, prop) { + if (prop === 'toString') return () => '' + return new Proxy({}, dryRunResolver()) + }, + } +} diff --git a/lib/step/meta.js b/lib/step/meta.js new file mode 100644 index 000000000..8cce6e2a8 --- /dev/null +++ b/lib/step/meta.js @@ -0,0 +1,91 @@ +const Step = require('../step') +const event = require('../event') +const { humanizeString } = require('../utils') + +class MetaStep extends Step { + constructor(obj, method) { + if (!method) method = '' + super(null, method) + this.actor = obj + } + + /** @return {boolean} */ + isBDD() { + if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) { + return true + } + return false + } + + toCliStyled() { + return this.toString() + } + + toString() { + const actorText = this.actor + + if (this.isBDD()) { + return `${this.prefix}${actorText} ${this.name} "${this.humanizeArgs()}${this.suffix}"` + } + + if (actorText === 'I') { + return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` + } + + return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` + } + + humanize() { + return humanizeString(this.name) + } + + setTrace() {} + + setContext(context) { + this.context = context + } + + /** @return {*} */ + run(fn) { + this.status = 'queued' + this.setArguments(Array.from(arguments).slice(1)) + let result + + const registerStep = step => { + this.setMetaStep(null) + step.setMetaStep(this) + } + event.dispatcher.prependListener(event.step.before, registerStep) + // Handle async and sync methods. + if (fn.constructor.name === 'AsyncFunction') { + result = fn + .apply(this.context, this.args) + .then(result => { + return result + }) + .catch(error => { + this.setStatus('failed') + throw error + }) + .finally(() => { + this.endTime = Date.now() + event.dispatcher.removeListener(event.step.before, registerStep) + }) + } else { + try { + this.startTime = Date.now() + result = fn.apply(this.context, this.args) + } catch (error) { + this.setStatus('failed') + throw error + } finally { + this.endTime = Date.now() + event.dispatcher.removeListener(event.step.before, registerStep) + } + } + + return result + } +} + +module.exports = MetaStep diff --git a/lib/step/record.js b/lib/step/record.js new file mode 100644 index 000000000..abe6d7ca5 --- /dev/null +++ b/lib/step/record.js @@ -0,0 +1,76 @@ +const event = require('../event') +const recorder = require('../recorder') +const StepConfig = require('./config') +const store = require('../store') +const { TIMEOUT_ORDER } = require('./timeout') + +function recordStep(step, args) { + step.status = 'queued' + + // apply step configuration + const lastArg = args[args.length - 1] + if (lastArg instanceof StepConfig) { + const stepConfig = args.pop() + const { options, timeout, retry } = stepConfig.getConfig() + + if (options) { + store.stepOptions = options + step.opts = options + } + if (timeout) step.setTimeout(timeout, TIMEOUT_ORDER.codeLimitTime) + if (retry) retryStep(retry) + } + + step.setArguments(args) + // run async before step hooks + event.emit(event.step.before, step) + + const task = `${step.name}: ${step.humanizeArgs()}` + let val + + // run step inside promise + recorder.add( + task, + () => { + if (!step.startTime) { + // step can be retries + event.emit(event.step.started, step) + step.startTime = Date.now() + } + return (val = step.run(...args)) + }, + false, + undefined, + step.timeout, + ) + + event.emit(event.step.after, step) + + recorder.add('step passed', () => { + step.endTime = Date.now() + event.emit(event.step.passed, step, val) + event.emit(event.step.finished, step) + }) + + recorder.catchWithoutStop(err => { + step.status = 'failed' + step.endTime = Date.now() + event.emit(event.step.failed, step) + event.emit(event.step.finished, step) + throw err + }) + + recorder.add('return result', () => val) + // run async after step hooks + + return recorder.promise() +} + +module.exports = recordStep + +function retryStep(opts) { + if (opts === undefined) opts = 1 + recorder.retry(opts) + // remove retry once the step passed + recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())) +} diff --git a/lib/step/timeout.js b/lib/step/timeout.js new file mode 100644 index 000000000..876644d41 --- /dev/null +++ b/lib/step/timeout.js @@ -0,0 +1,42 @@ +const TIMEOUT_ORDER = { + /** + * timeouts set with order below zero only override timeouts of higher order if their value is smaller + */ + testOrSuite: -5, + /** + * 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true + */ + stepTimeoutHard: 5, + /** + * 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation + */ + codeLimitTime: 15, + /** + * 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false + */ + stepTimeoutSoft: 25, +} + +function getCurrentTimeout(timeouts) { + let totalTimeout + // iterate over all timeouts starting from highest values of order + new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => { + if ( + timeout !== undefined && + // when orders >= 0 - timeout value overrides those set with higher order elements + (order >= 0 || + // when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order + totalTimeout === undefined || + // when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order + (timeout > 0 && (timeout < totalTimeout || totalTimeout === 0))) + ) { + totalTimeout = timeout + } + }) + return totalTimeout +} + +module.exports = { + TIMEOUT_ORDER, + getCurrentTimeout, +} diff --git a/lib/steps.js b/lib/steps.js new file mode 100644 index 000000000..fc126629d --- /dev/null +++ b/lib/steps.js @@ -0,0 +1,23 @@ +const StepConfig = require('./step/config') + +function stepOpts(opts = {}) { + return new StepConfig(opts) +} + +function stepTimeout(timeout) { + return new StepConfig().timeout(timeout) +} + +function stepRetry(retry) { + return new StepConfig().retry(retry) +} + +// Section function to be added here + +const step = { + opts: stepOpts, + timeout: stepTimeout, + retry: stepRetry, +} + +module.exports = step diff --git a/lib/utils.js b/lib/utils.js index 3c28b2696..2aac45685 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -531,3 +531,14 @@ module.exports.searchWithFusejs = function (source, searchString, opts) { return fuse.search(searchString) } + +module.exports.humanizeString = function (string) { + // split strings by words, then make them all lowercase + const _result = string + .replace(/([a-z](?=[A-Z]))/g, '$1 ') + .split(' ') + .map(word => word.toLowerCase()) + + _result[0] = _result[0] === 'i' ? this.ucfirst(_result[0]) : _result[0] + return _result.join(' ').trim() +} diff --git a/lib/within.js b/lib/within.js index 8893b4c74..119b45406 100644 --- a/lib/within.js +++ b/lib/within.js @@ -3,7 +3,7 @@ const store = require('./store') const recorder = require('./recorder') const container = require('./container') const event = require('./event') -const Step = require('./step') +const MetaStep = require('./step/meta') const { isAsyncFunction } = require('./utils') /** @@ -76,7 +76,7 @@ function within(context, fn) { module.exports = within -class WithinStep extends Step.MetaStep { +class WithinStep extends MetaStep { constructor(locator, fn) { super('Within') this.args = [locator] From 52d91f4cc14af836ed0eda4a88dbd4a86d3cc9a0 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 12 Jan 2025 23:23:59 +0200 Subject: [PATCH 03/12] refactored steps --- lib/step.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/step.js b/lib/step.js index 2a27ab4a7..8322bf54d 100644 --- a/lib/step.js +++ b/lib/step.js @@ -1,4 +1,6 @@ // refactored step class, moved to helper const Step = require('./step/helper') +const MetaStep = require('./step/meta') module.exports = Step +module.exports.MetaStep = MetaStep From 77031472d069cb23938a7ad4adffa8b95d2a75a7 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 12 Jan 2025 23:31:17 +0200 Subject: [PATCH 04/12] fixed refactoring --- lib/step/base.js | 9 --------- lib/step/meta.js | 4 ++-- lib/step/record.js | 9 +-------- lib/step/retry.js | 11 +++++++++++ 4 files changed, 14 insertions(+), 19 deletions(-) create mode 100644 lib/step/retry.js diff --git a/lib/step/base.js b/lib/step/base.js index f3303b606..c66a6cd9c 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -179,12 +179,3 @@ class Step { } module.exports = Step - -function dryRunResolver() { - return { - get(target, prop) { - if (prop === 'toString') return () => '' - return new Proxy({}, dryRunResolver()) - }, - } -} diff --git a/lib/step/meta.js b/lib/step/meta.js index 8cce6e2a8..91a0881ea 100644 --- a/lib/step/meta.js +++ b/lib/step/meta.js @@ -1,11 +1,11 @@ -const Step = require('../step') +const Step = require('./base') const event = require('../event') const { humanizeString } = require('../utils') class MetaStep extends Step { constructor(obj, method) { if (!method) method = '' - super(null, method) + super(method) this.actor = obj } diff --git a/lib/step/record.js b/lib/step/record.js index abe6d7ca5..1384bc78c 100644 --- a/lib/step/record.js +++ b/lib/step/record.js @@ -3,7 +3,7 @@ const recorder = require('../recorder') const StepConfig = require('./config') const store = require('../store') const { TIMEOUT_ORDER } = require('./timeout') - +const retryStep = require('./retry') function recordStep(step, args) { step.status = 'queued' @@ -67,10 +67,3 @@ function recordStep(step, args) { } module.exports = recordStep - -function retryStep(opts) { - if (opts === undefined) opts = 1 - recorder.retry(opts) - // remove retry once the step passed - recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())) -} diff --git a/lib/step/retry.js b/lib/step/retry.js new file mode 100644 index 000000000..d6a47317c --- /dev/null +++ b/lib/step/retry.js @@ -0,0 +1,11 @@ +const recorder = require('../recorder') +const event = require('../event') + +function retryStep(opts) { + if (opts === undefined) opts = 1 + recorder.retry(opts) + // remove retry once the step passed + recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())) +} + +module.exports = retryStep From d9af19d43f3dac060023954651a6cbdf4d093e34 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 00:22:08 +0200 Subject: [PATCH 05/12] fixed typings --- lib/step.js | 14 ++++++++++++++ lib/step/base.js | 5 ++--- lib/step/config.js | 22 +++++++++++++++++++++- lib/step/meta.js | 4 ++-- typings/index.d.ts | 3 --- typings/jsdoc.conf.js | 5 ++++- 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/step.js b/lib/step.js index 8322bf54d..9295a0835 100644 --- a/lib/step.js +++ b/lib/step.js @@ -1,6 +1,20 @@ // refactored step class, moved to helper +/** + * Step is wrapper around a helper method. + * It is used to create a new step that is a combination of other steps. + */ +const BaseStep = require('./step/base') +const StepConfig = require('./step/config') const Step = require('./step/helper') + +/** + * MetaStep is a step that is used to wrap other steps. + * It is used to create a new step that is a combination of other steps. + * It is used to create a new step that is a combination of other steps. + */ const MetaStep = require('./step/meta') module.exports = Step module.exports.MetaStep = MetaStep +module.exports.BaseStep = BaseStep +module.exports.StepConfig = StepConfig diff --git a/lib/step/base.js b/lib/step/base.js index c66a6cd9c..92030883c 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -8,7 +8,6 @@ const STACK_LINE = 4 /** * Each command in test executed through `I.` object is wrapped in Step. * Step allows logging executed commands and triggers hook before and after step execution. - * @param {CodeceptJS.Helper} helper * @param {string} name */ class Step { @@ -21,7 +20,7 @@ class Step { /** @member {Array<*>} */ this.args = [] - /** @member {StepConfig} */ + /** @member {Record} */ this.opts = {} /** @member {string} */ this.actor = 'I' // I = actor @@ -33,7 +32,7 @@ class Step { this.prefix = this.suffix = '' /** @member {string} */ this.comment = '' - /** @member {MetaStep} */ + /** @member {import('./meta')} */ this.metaStep = undefined /** @member {string} */ this.stack = '' diff --git a/lib/step/config.js b/lib/step/config.js index f4e6aa58d..739f342b1 100644 --- a/lib/step/config.js +++ b/lib/step/config.js @@ -1,5 +1,10 @@ +/** + * StepConfig is a configuration object for a step. + * It is used to create a new step that is a combination of other steps. + */ class StepConfig { - constructor(opts) { + constructor(opts = {}) { + /** @member {{ opts: Record, timeout: number|undefined, retry: number|undefined }} */ this.config = { opts, timeout: undefined, @@ -7,16 +12,31 @@ class StepConfig { } } + /** + * Set the options for the step. + * @param {object} opts - The options for the step. + * @returns {StepConfig} - The step configuration object. + */ opts(opts) { this.config.opts = opts return this } + /** + * Set the timeout for the step. + * @param {number} timeout - The timeout for the step. + * @returns {StepConfig} - The step configuration object. + */ timeout(timeout) { this.config.timeout = timeout return this } + /** + * Set the retry for the step. + * @param {number} retry - The retry for the step. + * @returns {StepConfig} - The step configuration object. + */ retry(retry) { this.config.retry = retry return this diff --git a/lib/step/meta.js b/lib/step/meta.js index 91a0881ea..bc2b0a39e 100644 --- a/lib/step/meta.js +++ b/lib/step/meta.js @@ -3,10 +3,10 @@ const event = require('../event') const { humanizeString } = require('../utils') class MetaStep extends Step { - constructor(obj, method) { + constructor(actor, method) { if (!method) method = '' super(method) - this.actor = obj + this.actor = actor } /** @return {boolean} */ diff --git a/typings/index.d.ts b/typings/index.d.ts index fb6a68aac..2bc730c77 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -451,9 +451,6 @@ declare namespace CodeceptJS { } // Extending JSDoc generated typings - interface Step { - isMetaStep(): this is MetaStep - } // Types who are not be defined by JSDoc type actor = void }>(customSteps?: T & ThisType>) => WithTranslation diff --git a/typings/jsdoc.conf.js b/typings/jsdoc.conf.js index 3ff9b4c04..ca1d115bf 100644 --- a/typings/jsdoc.conf.js +++ b/typings/jsdoc.conf.js @@ -16,7 +16,10 @@ module.exports = { './lib/recorder.js', './lib/secret.js', './lib/session.js', - './lib/step.js', + './lib/step/config.js', + './lib/step/base.js', + './lib/step/helper.js', + './lib/step/meta.js', './lib/store.js', './lib/mocha/ui.js', './lib/mocha/featureConfig.js', From 91d07cd54aaf6d5175b6cbab2d476c3b0e70ac2b Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 00:25:12 +0200 Subject: [PATCH 06/12] fixed typings --- lib/step/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/step/base.js b/lib/step/base.js index 92030883c..47bbe4f5c 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -32,7 +32,7 @@ class Step { this.prefix = this.suffix = '' /** @member {string} */ this.comment = '' - /** @member {import('./meta')} */ + /** @member {any} */ this.metaStep = undefined /** @member {string} */ this.stack = '' From 3510f2025b715b0f489eefd09abbb9158062efb9 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 00:28:59 +0200 Subject: [PATCH 07/12] added steps to export --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 44f16c6af..49562a469 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "exports": { ".": "./lib/index.js", "./els": "./lib/els.js", - "./effects": "./lib/effects.js" + "./effects": "./lib/effects.js", + "./steps": "./lib/steps.js" }, "types": "typings/index.d.ts", "bin": { From 9151b8cbe48fd31d81c8977d3dc241b4191d3f59 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 18:30:12 +0200 Subject: [PATCH 08/12] added step timeouts tests --- lib/listener/globalTimeout.js | 2 +- lib/step/record.js | 15 +++-- runok.js | 67 +++++++++++++++++++ .../step-enhancements/codecept.conf.js | 14 ++++ .../step-enhancements/custom_helper.js | 24 +++++++ .../step-enhancements_test.js | 14 ++++ .../sandbox/configs/timeouts/suite_test.js | 23 ++++--- test/runner/step-enhancements_test.js | 42 ++++++++++++ 8 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 test/data/sandbox/configs/step-enhancements/codecept.conf.js create mode 100644 test/data/sandbox/configs/step-enhancements/custom_helper.js create mode 100644 test/data/sandbox/configs/step-enhancements/step-enhancements_test.js create mode 100644 test/runner/step-enhancements_test.js diff --git a/lib/listener/globalTimeout.js b/lib/listener/globalTimeout.js index 0aad80c9f..fc77decc5 100644 --- a/lib/listener/globalTimeout.js +++ b/lib/listener/globalTimeout.js @@ -3,7 +3,7 @@ const output = require('../output') const recorder = require('../recorder') const Config = require('../config') const { timeouts } = require('../store') -const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER +const { TIMEOUT_ORDER } = require('../step/timeout') module.exports = function () { let timeout diff --git a/lib/step/record.js b/lib/step/record.js index 1384bc78c..40922b401 100644 --- a/lib/step/record.js +++ b/lib/step/record.js @@ -1,6 +1,7 @@ const event = require('../event') const recorder = require('../recorder') const StepConfig = require('./config') +const { debug } = require('../output') const store = require('../store') const { TIMEOUT_ORDER } = require('./timeout') const retryStep = require('./retry') @@ -11,13 +12,17 @@ function recordStep(step, args) { const lastArg = args[args.length - 1] if (lastArg instanceof StepConfig) { const stepConfig = args.pop() - const { options, timeout, retry } = stepConfig.getConfig() + const { opts, timeout, retry } = stepConfig.getConfig() - if (options) { - store.stepOptions = options - step.opts = options + if (opts) { + debug(`Step ${step.name}: options applied ${JSON.stringify(opts)}`) + store.stepOptions = opts + step.opts = opts + } + if (timeout) { + debug(`Step ${step.name} timeout ${timeout}s`) + step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime) } - if (timeout) step.setTimeout(timeout, TIMEOUT_ORDER.codeLimitTime) if (retry) retryStep(retry) } diff --git a/runok.js b/runok.js index 2549582ac..8bfa171db 100755 --- a/runok.js +++ b/runok.js @@ -546,6 +546,73 @@ ${changelog}` process.exit(1) } }, + + async runnerCreateTests(featureName) { + // create runner tests for feature + const fs = require('fs').promises + const path = require('path') + + // Create directories + const configDir = path.join('test/data/sandbox/configs', featureName) + await fs.mkdir(configDir, { recursive: true }) + + // Create codecept config file + const configContent = `exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + mocha: {}, + name: '${featureName} tests' + } + ` + await fs.writeFile(path.join(configDir, `codecept.conf.js`), configContent) + + // Create feature test file + const testContent = `Feature('${featureName}'); + +Scenario('test ${featureName}', ({ I }) => { + // Add test steps here +}); +` + await fs.writeFile(path.join(configDir, `${featureName}_test.js`), testContent) + + // Create runner test file + const runnerTestContent = `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, verbose = false) => + \`\${codecept_run} \${verbose ? '--verbose' : ''} --config \${codecept_dir}/configs/${featureName}/\${config} \${grep ? \`--grep "\${grep}"\` : ''}\` + +describe('CodeceptJS ${featureName}', function () { + this.timeout(10000) + + it('should run ${featureName} test', done => { + exec(config_run_config('codecept.conf.js'), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) +}) +` + await fs.writeFile(path.join('test/runner', `${featureName}_test.js`), runnerTestContent) + + 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('') + console.log('Run tests with:') + console.log(`npx mocha test/runner --grep ${featureName}`) + }, } async function processChangelog() { diff --git a/test/data/sandbox/configs/step-enhancements/codecept.conf.js b/test/data/sandbox/configs/step-enhancements/codecept.conf.js new file mode 100644 index 000000000..8ec600fbf --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/codecept.conf.js @@ -0,0 +1,14 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + CustomHelper: { + require: './custom_helper.js', + }, + }, + include: {}, + bootstrap: false, + mocha: {}, + name: 'step-enhancements tests', +} diff --git a/test/data/sandbox/configs/step-enhancements/custom_helper.js b/test/data/sandbox/configs/step-enhancements/custom_helper.js new file mode 100644 index 000000000..7ce5002fb --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/custom_helper.js @@ -0,0 +1,24 @@ +const { store } = require('codeceptjs') + +let retryCount = 0 + +class MyHelper { + retryFewTimesAndPass(num) { + if (retryCount < num) { + retryCount++ + throw new Error('Failed on try ' + retryCount) + } + } + + wait(timeout) { + return new Promise(resolve => setTimeout(resolve, timeout)) + } + + printOption() { + if (store.currentStep?.opts) { + console.log('Option:', store.currentStep?.opts?.text) + } + } +} + +module.exports = MyHelper diff --git a/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js b/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js new file mode 100644 index 000000000..7e949fe75 --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js @@ -0,0 +1,14 @@ +const step = require('codeceptjs/steps') +Feature('step-enhancements') + +Scenario('test step opts', ({ I }) => { + I.printOption(step.opts({ text: 'Hello' })) +}) + +Scenario('test step timeouts', ({ I }) => { + I.wait(1000, step.timeout(0.1)) +}) + +Scenario('test step retry', ({ I }) => { + I.retryFewTimesAndPass(3, step.retry(4)) +}) diff --git a/test/data/sandbox/configs/timeouts/suite_test.js b/test/data/sandbox/configs/timeouts/suite_test.js index 64a2e015e..1ffaff859 100644 --- a/test/data/sandbox/configs/timeouts/suite_test.js +++ b/test/data/sandbox/configs/timeouts/suite_test.js @@ -1,14 +1,21 @@ -Feature('no timeout'); +const step = require('codeceptjs/steps') + +Feature('no timeout') Scenario('no timeout test #first', ({ I }) => { - I.waitForSleep(1000); -}); + I.waitForSleep(1000) +}) Scenario('timeout test in 0.5 #second', { timeout: 0.5 }, ({ I }) => { - I.waitForSleep(1000); -}); + I.waitForSleep(1000) +}) Scenario('timeout step in 0.5', ({ I }) => { - I.limitTime(0.2).waitForSleep(100); - I.limitTime(0.1).waitForSleep(3000); -}); + I.limitTime(0.2).waitForSleep(100) + I.limitTime(0.1).waitForSleep(3000) +}) + +Scenario('timeout step in 0.5 new syntax', ({ I }) => { + I.waitForSleep(100, step.timeout(0.2)) + I.waitForSleep(3000, step.timeout(0.1)) +}) diff --git a/test/runner/step-enhancements_test.js b/test/runner/step-enhancements_test.js new file mode 100644 index 000000000..b988562e4 --- /dev/null +++ b/test/runner/step-enhancements_test.js @@ -0,0 +1,42 @@ +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, verbose = false) => `${codecept_run} ${verbose ? '--verbose' : ''} --config ${codecept_dir}/configs/step-enhancements/${config} ${grep ? `--grep "${grep}"` : ''}` + +describe('CodeceptJS step-enhancements', function () { + this.timeout(10000) + + it('should apply step options', done => { + exec(config_run_config('codecept.conf.js', 'opts', true), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('Option: Hello') + expect(stdout).toContain('options applied {"text":"Hello"}') + expect(stdout).toContain('print option') + expect(stdout).not.toContain('print option {"text":"Hello"}') + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) + + it('should apply step timeouts', done => { + exec(config_run_config('codecept.conf.js', 'timeouts', true), (err, stdout) => { + debug(stdout) + expect(err).toBeTruthy() + expect(stdout).not.toContain('OK') + expect(stdout).toContain('was interrupted on step timeout 100ms') + done() + }) + }) + + it('should apply step retry', done => { + exec(config_run_config('codecept.conf.js', 'retry', true), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) +}) From 76e43e5a1a302072d4970952221c2aa9a919fd86 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 18:31:48 +0200 Subject: [PATCH 09/12] deprecated I.limitTime and I.retry --- lib/actor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/actor.js b/lib/actor.js index 15e0fe3f5..ff3a54050 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -42,6 +42,8 @@ class Actor { limitTime(timeout) { if (!store.timeouts) return this + console.log('I.limitTime() is deprecated, use step.timeout() instead') + event.dispatcher.prependOnceListener(event.step.before, step => { output.log(`Timeout to ${step}: ${timeout}s`) step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime) @@ -57,6 +59,7 @@ class Actor { * @inner */ retry(opts) { + console.log('I.retry() is deprecated, use step.retry() instead') const retryStep = require('./step/retry') retryStep(opts) return this From 38123e92ff1337137ea954534e0049f8f93f67bf Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 19:02:53 +0200 Subject: [PATCH 10/12] fix failed timeout tests --- lib/plugin/stepTimeout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugin/stepTimeout.js b/lib/plugin/stepTimeout.js index 27ed98a94..36f06d5c1 100644 --- a/lib/plugin/stepTimeout.js +++ b/lib/plugin/stepTimeout.js @@ -1,5 +1,5 @@ const event = require('../event') -const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER +const { TIMEOUT_ORDER } = require('../step/timeout') const defaultConfig = { timeout: 150, From 7f77dc37cc3c8def5c242f871eae462e780d32da Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Jan 2025 19:27:14 +0200 Subject: [PATCH 11/12] added test for case insensitive opt --- lib/helper/Playwright.js | 1 - lib/helper/Puppeteer.js | 5 +++++ lib/helper/WebDriver.js | 10 +++++++++- test/helper/webapi.js | 13 ++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index d6cb11459..01e8bcc40 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -3500,7 +3500,6 @@ async function proceedSee(assertType, text, context, strict = false) { } if (store?.currentStep?.opts?.ignoreCase === true) { - description = description.toLowerCase() text = text.toLowerCase() allText = allText.map(elText => elText.toLowerCase()) } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ce65db079..3ea66872d 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2769,6 +2769,11 @@ async function proceedSee(assertType, text, context, strict = false) { allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue()))) } + if (store?.currentStep?.opts?.ignoreCase === true) { + text = text.toLowerCase() + allText = allText.map(elText => elText.toLowerCase()) + } + if (strict) { return allText.map(elText => equals(description)[assertType](text, elText)) } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index c0e5bcaa5..e55b51cf8 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -7,6 +7,7 @@ const Helper = require('@codeceptjs/helper') const promiseRetry = require('promise-retry') const stringIncludes = require('../assert/include').includes const { urlEquals, equals } = require('../assert/equal') +const store = require('../store') const { debug } = require('../output') const { empty } = require('../assert/empty') const { truth } = require('../assert/truth') @@ -2698,7 +2699,14 @@ async function proceedSee(assertType, text, context, strict = false) { const smartWaitEnabled = assertType === 'assert' const res = await this._locate(withStrictLocator(context), smartWaitEnabled) assertElementExists(res, context) - const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el))) + let selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el))) + + // apply ignoreCase option + if (store?.currentStep?.opts?.ignoreCase === true) { + text = text.toLowerCase() + selected = selected.map(elText => elText.toLowerCase()) + } + if (strict) { if (Array.isArray(selected) && selected.length !== 0) { return selected.map(elText => equals(description)[assertType](text, elText)) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 87592c343..489dcad1d 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1,5 +1,5 @@ const chai = require('chai') - +const store = require('../../lib/store') const expect = chai.expect const assert = chai.assert const path = require('path') @@ -101,6 +101,17 @@ module.exports.tests = function () { await I.dontSee('Info') }) + it('should check text on site with ignoreCase option', async () => { + if (isHelper('TestCafe')) return // It won't be implemented + await I.amOnPage('/') + await I.see('Welcome') + store.currentStep = { opts: { ignoreCase: true } } + await I.see('welcome to test app!') + await I.see('test link', 'a') + store.currentStep = {} + await I.dontSee('welcome') + }) + it('should check text inside element', async () => { await I.amOnPage('/') await I.see('Welcome to test app!', 'h1') From a03cd8867af10e2bb47e96c3b68634d167e23f0c Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 14 Jan 2025 01:33:23 +0200 Subject: [PATCH 12/12] fixed flakiness of timeout tests --- test/data/sandbox/configs/timeouts/suite_test.js | 4 ++-- test/runner/timeout_test.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/data/sandbox/configs/timeouts/suite_test.js b/test/data/sandbox/configs/timeouts/suite_test.js index 1ffaff859..f53f2bfb7 100644 --- a/test/data/sandbox/configs/timeouts/suite_test.js +++ b/test/data/sandbox/configs/timeouts/suite_test.js @@ -12,10 +12,10 @@ Scenario('timeout test in 0.5 #second', { timeout: 0.5 }, ({ I }) => { Scenario('timeout step in 0.5', ({ I }) => { I.limitTime(0.2).waitForSleep(100) - I.limitTime(0.1).waitForSleep(3000) + I.limitTime(0.2).waitForSleep(3000) }) Scenario('timeout step in 0.5 new syntax', ({ I }) => { I.waitForSleep(100, step.timeout(0.2)) - I.waitForSleep(3000, step.timeout(0.1)) + I.waitForSleep(3000, step.timeout(0.2)) }) diff --git a/test/runner/timeout_test.js b/test/runner/timeout_test.js index 5e640f63c..a3c1ffefe 100644 --- a/test/runner/timeout_test.js +++ b/test/runner/timeout_test.js @@ -40,6 +40,7 @@ describe('CodeceptJS Timeouts', function () { }) it('should use global timeouts if timeout is set', done => { + this.retries(3) exec(config_run_config('codecept.timeout.conf.js', 'no timeout test'), (err, stdout) => { debug_this_test && console.log(stdout) expect(stdout).toContain('Timeout 0.1') @@ -51,7 +52,7 @@ describe('CodeceptJS Timeouts', function () { it('should prefer step timeout', done => { exec(config_run_config('codecept.conf.js', 'timeout step', true), (err, stdout) => { debug_this_test && console.log(stdout) - expect(stdout).toContain('was interrupted on step timeout 100ms') + expect(stdout).toContain('was interrupted on step timeout 200ms') expect(err).toBeTruthy() done() })