From 2c32d0d11438492457aa7f1286e5aab2780a2dfd Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 6 Jan 2025 05:25:11 +0200 Subject: [PATCH 1/7] fixed pause finishing, added suggest for empty run, included fuse.js for better search --- lib/codecept.js | 1 + lib/listener/emptyRun.js | 57 ++++++++ lib/mocha/factory.js | 3 - lib/mocha/test.js | 2 + lib/mocha/types.d.ts | 1 + lib/pause.js | 281 +++++++++++++++++++++------------------ lib/recorder.js | 263 +++++++++++++++++++----------------- package.json | 1 + 8 files changed, 355 insertions(+), 254 deletions(-) create mode 100644 lib/listener/emptyRun.js diff --git a/lib/codecept.js b/lib/codecept.js index b924425ee..f0626cbcc 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -111,6 +111,7 @@ class Codecept { runHook(require('./listener/globalTimeout')) runHook(require('./listener/globalRetry')) runHook(require('./listener/exit')) + runHook(require('./listener/emptyRun')) // custom hooks (previous iteration of plugins) this.config.hooks.forEach(hook => runHook(hook)) diff --git a/lib/listener/emptyRun.js b/lib/listener/emptyRun.js new file mode 100644 index 000000000..5b128989f --- /dev/null +++ b/lib/listener/emptyRun.js @@ -0,0 +1,57 @@ +const figures = require('figures') +const Container = require('../container') +const event = require('../event') +const output = require('../output') + +module.exports = function () { + let isEmptyRun = true + + event.dispatcher.on(event.test.before, test => { + isEmptyRun = false + }) + + event.dispatcher.on(event.all.result, () => { + if (isEmptyRun) { + const mocha = Container.mocha() + + if (mocha.options.grep) { + const Fuse = require('fuse.js') + + output.print() + output.print('No tests found by pattern: ' + mocha.options.grep) + + const allTests = [] + mocha.suite.suites.forEach(suite => { + suite.tests.forEach(test => { + allTests.push(test.fullTitle()) + }) + }) + + const fuse = new Fuse(allTests, { + includeScore: true, + threshold: 0.6, + caseSensitive: false, + }) + + const results = fuse.search(mocha.options.grep.toString()) + + if (results.length > 0) { + output.print() + output.print('Maybe you wanted to run one of these tests?') + results.forEach(result => { + output.print(figures.checkboxOff, output.styles.log(result.item)) + }) + + output.print() + output.print(output.styles.debug('To run the first test use the following command:')) + output.print(output.styles.bold('npx codeceptjs run --debug --grep "' + results[0].item + '"')) + } + } + if (process.env.CI) { + output.print() + output.error('No tests were executed. Failing on CI') + process.exitCode = 1 + } + } + }) +} diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js index d5ef1444e..3ae4b5613 100644 --- a/lib/mocha/factory.js +++ b/lib/mocha/factory.js @@ -4,7 +4,6 @@ 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') @@ -45,8 +44,6 @@ class MochaFactory { let missingFeatureInFile = [] const seenTests = [] mocha.suite.eachTest(test => { - test.uid = genTestId(test) - const name = test.fullTitle() if (seenTests.includes(test.uid)) { dupes.push(name) diff --git a/lib/mocha/test.js b/lib/mocha/test.js index 836f1f6cf..48908a308 100644 --- a/lib/mocha/test.js +++ b/lib/mocha/test.js @@ -1,6 +1,7 @@ const Test = require('mocha/lib/test') const { test: testWrapper } = require('./asyncWrapper') const { enhanceMochaSuite } = require('./suite') +const { genTestId } = require('../utils') /** * Factory function to create enhanced tests @@ -40,6 +41,7 @@ function enhanceMochaTest(test) { suite.addTest(testWrapper(this)) test.tags = [...(test.tags || []), ...(suite.tags || [])] test.fullTitle = () => `${suite.title}: ${test.title}` + test.uid = genTestId(test) } test.applyOptions = function (opts) { diff --git a/lib/mocha/types.d.ts b/lib/mocha/types.d.ts index 36e3e72ce..50c51e2ef 100644 --- a/lib/mocha/types.d.ts +++ b/lib/mocha/types.d.ts @@ -3,6 +3,7 @@ import { Test as MochaTest, Suite as MochaSuite } from 'mocha' declare global { namespace CodeceptJS { interface Test extends MochaTest { + uid: string title: string tags: string[] steps: string[] diff --git a/lib/pause.js b/lib/pause.js index 7f5830480..b4bbf670b 100644 --- a/lib/pause.js +++ b/lib/pause.js @@ -1,59 +1,70 @@ -const colors = require('chalk'); -const readline = require('readline'); -const ora = require('ora-classic'); -const debug = require('debug')('codeceptjs:pause'); - -const container = require('./container'); -const history = require('./history'); -const store = require('./store'); -const aiAssistant = require('./ai'); -const recorder = require('./recorder'); -const event = require('./event'); -const output = require('./output'); -const { methodsOfObject } = require('./utils'); +const colors = require('chalk') +const readline = require('readline') +const ora = require('ora-classic') +const debug = require('debug')('codeceptjs:pause') +const Fuse = require('fuse.js') + +const container = require('./container') +const history = require('./history') +const store = require('./store') +const aiAssistant = require('./ai') +const recorder = require('./recorder') +const event = require('./event') +const output = require('./output') +const { methodsOfObject } = require('./utils') // npm install colors -let rl; -let nextStep; -let finish; -let next; -let registeredVariables = {}; +let rl +let nextStep +let finish +let next +let registeredVariables = {} /** * Pauses test execution and starts interactive shell * @param {Object} [passedObject] */ const pause = function (passedObject = {}) { - if (store.dryRun) return; + if (store.dryRun) return - next = false; + next = false // add listener to all next steps to provide next() functionality event.dispatcher.on(event.step.after, () => { recorder.add('Start next pause session', () => { - if (!next) return; - return pauseSession(); - }); - }); - recorder.add('Start new session', () => pauseSession(passedObject)); -}; + // test already finished, nothing to pause + if (!store.currentTest) return + if (!next) return + return pauseSession() + }) + }) + + event.dispatcher.on(event.test.finished, () => { + finish() + recorder.session.restore('pause') + rl.close() + history.save() + }) + + recorder.add('Start new session', () => pauseSession(passedObject)) +} function pauseSession(passedObject = {}) { - registeredVariables = passedObject; - recorder.session.start('pause'); + registeredVariables = passedObject + recorder.session.start('pause') if (!next) { - let vars = Object.keys(registeredVariables).join(', '); - if (vars) vars = `(vars: ${vars})`; + let vars = Object.keys(registeredVariables).join(', ') + if (vars) vars = `(vars: ${vars})` - output.print(colors.yellow(' Interactive shell started')); - output.print(colors.yellow(' Use JavaScript syntax to try steps in action')); - output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`)); - output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`)); - output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`)); - output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`)); + output.print(colors.yellow(' Interactive shell started')) + output.print(colors.yellow(' Use JavaScript syntax to try steps in action')) + output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`)) + output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`)) + output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`)) + output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`)) if (aiAssistant.isEnabled) { - output.print(colors.blue(` ${colors.bold('AI is enabled! (experimental)')} Write what you want and make AI run it`)); - output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to AI provider')); - output.print(colors.blue(' Ideas: ask it to fill forms for you or to click')); + output.print(colors.blue(` ${colors.bold('AI is enabled! (experimental)')} Write what you want and make AI run it`)) + output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to AI provider')) + output.print(colors.blue(' Ideas: ask it to fill forms for you or to click')) } } @@ -64,155 +75,165 @@ function pauseSession(passedObject = {}) { completer, history: history.load(), historySize: 50, // Adjust the history size as needed - }); + }) - rl.on('line', parseInput); + rl.on('line', parseInput) rl.on('close', () => { - if (!next) console.log('Exiting interactive shell....'); - }); + if (!next) console.log('Exiting interactive shell....') + }) return new Promise(resolve => { - finish = resolve; + finish = resolve // eslint-disable-next-line - return askForStep(); - }); + return askForStep() + }) } /* eslint-disable */ async function parseInput(cmd) { - rl.pause(); - next = false; - recorder.session.start('pause'); - if (cmd === '') next = true; + rl.pause() + next = false + recorder.session.start('pause') + if (cmd === '') next = true if (!cmd || cmd === 'resume' || cmd === 'exit') { - finish(); - recorder.session.restore(); - rl.close(); - history.save(); - return nextStep(); + finish() + recorder.session.restore('pause') + rl.close() + history.save() + return nextStep() } for (const k of Object.keys(registeredVariables)) { - eval(`var ${k} = registeredVariables['${k}'];`); + eval(`var ${k} = registeredVariables['${k}'];`) } - let executeCommand = Promise.resolve(); + let executeCommand = Promise.resolve() const getCmd = () => { - debug('Command:', cmd); - return cmd; - }; - - let isCustomCommand = false; - let lastError = null; - let isAiCommand = false; - let $res; + debug('Command:', cmd) + return cmd + } + + let isCustomCommand = false + let lastError = null + let isAiCommand = false + let $res try { - const locate = global.locate; // enable locate in this context + const locate = global.locate // enable locate in this context - const I = container.support('I'); + const I = container.support('I') if (cmd.trim().startsWith('=>')) { - isCustomCommand = true; - cmd = cmd.trim().substring(2, cmd.length); + isCustomCommand = true + cmd = cmd.trim().substring(2, cmd.length) } else if (aiAssistant.isEnabled && cmd.trim() && !cmd.match(/^\w+\(/) && cmd.includes(' ')) { - const currentOutputLevel = output.level(); - output.level(0); - const res = I.grabSource(); - isAiCommand = true; + const currentOutputLevel = output.level() + output.level(0) + const res = I.grabSource() + isAiCommand = true executeCommand = executeCommand.then(async () => { try { - const html = await res; - await aiAssistant.setHtmlContext(html); + const html = await res + await aiAssistant.setHtmlContext(html) } catch (err) { - output.print(output.styles.error(' ERROR '), "Can't get HTML context", err.stack); - return; + output.print(output.styles.error(' ERROR '), "Can't get HTML context", err.stack) + return } finally { - output.level(currentOutputLevel); + output.level(currentOutputLevel) } - const spinner = ora('Processing AI request...').start(); - cmd = await aiAssistant.writeSteps(cmd); - spinner.stop(); - output.print(''); - output.print(colors.blue(aiAssistant.getResponse())); - output.print(''); - return cmd; - }); + const spinner = ora('Processing AI request...').start() + cmd = await aiAssistant.writeSteps(cmd) + spinner.stop() + output.print('') + output.print(colors.blue(aiAssistant.getResponse())) + output.print('') + return cmd + }) } else { - cmd = `I.${cmd}`; + cmd = `I.${cmd}` } executeCommand = executeCommand .then(async () => { - const cmd = getCmd(); - if (!cmd) return; - return eval(cmd); + const cmd = getCmd() + if (!cmd) return + return eval(cmd) }) .catch(err => { - debug(err); - if (isAiCommand) return; - if (!lastError) output.print(output.styles.error(' ERROR '), err.message); - debug(err.stack); + debug(err) + if (isAiCommand) return + if (!lastError) output.print(output.styles.error(' ERROR '), err.message) + debug(err.stack) - lastError = err.message; - }); + lastError = err.message + }) - const val = await executeCommand; + const val = await executeCommand if (isCustomCommand) { - if (val !== undefined) console.log('Result', '$res=', val); - $res = val; + if (val !== undefined) console.log('Result', '$res=', val) + $res = val } if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) { - output.print(output.styles.success(' OK '), cmd); + output.print(output.styles.success(' OK '), cmd) } if (cmd?.startsWith('I.grab')) { - output.print(output.styles.debug(val)); + output.print(output.styles.debug(val)) } - history.push(cmd); // add command to history when successful + history.push(cmd) // add command to history when successful } catch (err) { - if (!lastError) output.print(output.styles.error(' ERROR '), err.message); - lastError = err.message; + if (!lastError) output.print(output.styles.error(' ERROR '), err.message) + lastError = err.message } recorder.session.catch(err => { - const msg = err.cliMessage ? err.cliMessage() : err.message; + const msg = err.cliMessage ? err.cliMessage() : err.message // pop latest command from history because it failed - history.pop(); - - if (isAiCommand) return; - if (!lastError) output.print(output.styles.error(' FAIL '), msg); - lastError = err.message; - }); - recorder.add('ask for next step', askForStep); - nextStep(); + history.pop() + + if (isAiCommand) return + if (!lastError) output.print(output.styles.error(' FAIL '), msg) + lastError = err.message + }) + recorder.add('ask for next step', askForStep) + nextStep() } /* eslint-enable */ function askForStep() { return new Promise(resolve => { - nextStep = resolve; - rl.setPrompt(' I.', 3); - rl.resume(); - rl.prompt([false]); - }); + nextStep = resolve + rl.setPrompt(' I.', 3) + rl.resume() + rl.prompt([false]) + }) } function completer(line) { - const I = container.support('I'); - const completions = methodsOfObject(I); - const hits = completions.filter(c => { - if (c.indexOf(line) === 0) { - return c; - } - return null; - }); - return [hits && hits.length ? hits : completions, line]; + const I = container.support('I') + const completions = methodsOfObject(I) + // If no input, return all completions + if (!line) { + return [completions, line] + } + + // Initialize Fuse with completions + const fuse = new Fuse(completions, { + threshold: 0.3, + distance: 100, + minMatchCharLength: 1, + }) + + // Search using Fuse.js + const searchResults = fuse.search(line) + const hits = searchResults.map(result => result.item) + + return [hits, line] } function registerVariable(name, value) { - registeredVariables[name] = value; + registeredVariables[name] = value } -module.exports = pause; +module.exports = pause -module.exports.registerVariable = registerVariable; +module.exports.registerVariable = registerVariable diff --git a/lib/recorder.js b/lib/recorder.js index e3fb80a4b..40db146c7 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -1,27 +1,27 @@ -const debug = require('debug')('codeceptjs:recorder'); -const promiseRetry = require('promise-retry'); -const chalk = require('chalk'); -const { printObjectProperties } = require('./utils'); -const { log } = require('./output'); +const debug = require('debug')('codeceptjs:recorder') +const promiseRetry = require('promise-retry') +const chalk = require('chalk') +const { printObjectProperties } = require('./utils') +const { log } = require('./output') -const MAX_TASKS = 100; +const MAX_TASKS = 100 -let promise; -let running = false; -let errFn; -let queueId = 0; -let sessionId = null; -let asyncErr = null; -let ignoredErrs = []; +let promise +let running = false +let errFn +let queueId = 0 +let sessionId = null +let asyncErr = null +let ignoredErrs = [] -let tasks = []; -let oldPromises = []; +let tasks = [] +let oldPromises = [] const defaultRetryOptions = { retries: 0, minTimeout: 150, maxTimeout: 10000, -}; +} /** * Singleton object to record all test steps as promises and run them in chain. @@ -29,7 +29,6 @@ const defaultRetryOptions = { * @interface */ module.exports = { - /** * @type {Array>} * @inner @@ -43,11 +42,11 @@ module.exports = { * @inner */ start() { - debug('Starting recording promises'); - running = true; - asyncErr = null; - errFn = null; - this.reset(); + debug('Starting recording promises') + running = true + asyncErr = null + errFn = null + this.reset() }, /** @@ -55,7 +54,7 @@ module.exports = { * @inner */ isRunning() { - return running; + return running }, /** @@ -64,7 +63,7 @@ module.exports = { */ startUnlessRunning() { if (!this.isRunning()) { - this.start(); + this.start() } }, @@ -76,7 +75,7 @@ module.exports = { * @inner */ errHandler(fn) { - errFn = fn; + errFn = fn }, /** @@ -87,16 +86,16 @@ module.exports = { * @inner */ reset() { - if (promise && running) this.catch(); - queueId++; - sessionId = null; - asyncErr = null; - log(`${currentQueue()} Starting recording promises`); - promise = Promise.resolve(); - oldPromises = []; - tasks = []; - ignoredErrs = []; - this.session.running = false; + if (promise && running) this.catch() + queueId++ + sessionId = null + asyncErr = null + log(`${currentQueue()} Starting recording promises`) + promise = Promise.resolve() + oldPromises = [] + tasks = [] + ignoredErrs = [] + this.session.running = false // reset this retries makes the retryFailedStep plugin won't work if there is Before/BeforeSuit block due to retries is undefined on Scenario // this.retries = []; }, @@ -123,12 +122,16 @@ module.exports = { * @inner */ start(name) { - debug(`${currentQueue()}Starting <${name}> session`); - tasks.push('--->'); - oldPromises.push(promise); - this.running = true; - sessionId = name; - promise = Promise.resolve(); + if (sessionId) { + debug(`${currentQueue()}Session already started as ${sessionId}`) + this.restore(sessionId) + } + debug(`${currentQueue()}Starting <${name}> session`) + tasks.push('--->') + oldPromises.push(promise) + this.running = true + sessionId = name + promise = Promise.resolve() }, /** @@ -136,12 +139,12 @@ module.exports = { * @inner */ restore(name) { - tasks.push('<---'); - debug(`${currentQueue()}Finalize <${name}> session`); - this.running = false; - sessionId = null; - this.catch(errFn); - promise = promise.then(() => oldPromises.pop()); + tasks.push('<---') + debug(`${currentQueue()}Finalize <${name}> session`) + this.running = false + sessionId = null + this.catch(errFn) + promise = promise.then(() => oldPromises.pop()) }, /** @@ -149,9 +152,8 @@ module.exports = { * @inner */ catch(fn) { - promise = promise.catch(fn); + promise = promise.catch(fn) }, - }, /** @@ -171,42 +173,47 @@ module.exports = { */ add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) { if (typeof taskName === 'function') { - fn = taskName; - taskName = fn.toString(); - if (retry === undefined) retry = false; + fn = taskName + taskName = fn.toString() + if (retry === undefined) retry = false } - if (retry === undefined) retry = true; + if (retry === undefined) retry = true if (!running && !force) { - return; + return } - tasks.push(taskName); - debug(chalk.gray(`${currentQueue()} Queued | ${taskName}`)); + tasks.push(taskName) + debug(chalk.gray(`${currentQueue()} Queued | ${taskName}`)) - return promise = Promise.resolve(promise).then((res) => { + return (promise = Promise.resolve(promise).then(res => { // prefer options for non-conditional retries - const retryOpts = this.retries.sort((r1, r2) => r1.when && !r2.when).slice(-1).pop(); + const retryOpts = this.retries + .sort((r1, r2) => r1.when && !r2.when) + .slice(-1) + .pop() // no retries or unnamed tasks if (!retryOpts || !taskName || !retry) { - const [promise, timer] = getTimeoutPromise(timeout, taskName); - return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)); + const [promise, timer] = getTimeoutPromise(timeout, taskName) + return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)) } - debug(`${currentQueue()} Running | ${taskName}`); + debug(`${currentQueue()} Running | ${taskName}`) - const retryRules = this.retries.slice().reverse(); + const retryRules = this.retries.slice().reverse() return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => { - if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`); - const [promise, timer] = getTimeoutPromise(timeout, taskName); - return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => { - if (ignoredErrs.includes(err)) return; - for (const retryObj of retryRules) { - if (!retryObj.when) return retry(err); - if (retryObj.when && retryObj.when(err)) return retry(err); - } - throw err; - }); - }); - }); + if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`) + const [promise, timer] = getTimeoutPromise(timeout, taskName) + return Promise.race([promise, Promise.resolve(res).then(fn)]) + .finally(() => clearTimeout(timer)) + .catch(err => { + if (ignoredErrs.includes(err)) return + for (const retryObj of retryRules) { + if (!retryObj.when) return retry(err) + if (retryObj.when && retryObj.when(err)) return retry(err) + } + throw err + }) + }) + })) }, /** @@ -215,15 +222,15 @@ module.exports = { * @inner */ retry(opts) { - if (!promise) return; + if (!promise) return if (opts === null) { - opts = {}; + opts = {} } if (Number.isInteger(opts)) { - opts = { retries: opts }; + opts = { retries: opts } } - return this.add(() => this.retries.push(opts)); + return this.add(() => this.retries.push(opts)) }, /** @@ -232,20 +239,25 @@ module.exports = { * @inner */ catch(customErrFn) { - const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50); - debug(chalk.gray(`${currentQueue()} Queued | catch with error handler ${fnDescription || ''}`)); - return promise = promise.catch((err) => { - log(`${currentQueue()}Error | ${err} ${fnDescription}...`); - if (!(err instanceof Error)) { // strange things may happen - err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`); // we should be prepared for them + const fnDescription = customErrFn + ?.toString() + ?.replace(/\s{2,}/g, ' ') + .replace(/\n/g, ' ') + ?.slice(0, 50) + debug(chalk.gray(`${currentQueue()} Queued | catch with error handler ${fnDescription || ''}`)) + return (promise = promise.catch(err => { + log(`${currentQueue()}Error | ${err} ${fnDescription}...`) + if (!(err instanceof Error)) { + // strange things may happen + err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`) // we should be prepared for them } if (customErrFn) { - customErrFn(err); + customErrFn(err) } else if (errFn) { - errFn(err); + errFn(err) } - this.stop(); - }); + this.stop() + })) }, /** @@ -254,17 +266,22 @@ module.exports = { * @inner */ catchWithoutStop(customErrFn) { - const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50); - return promise = promise.catch((err) => { - if (ignoredErrs.includes(err)) return; // already caught - log(`${currentQueue()} Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`); - if (!(err instanceof Error)) { // strange things may happen - err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them + const fnDescription = customErrFn + ?.toString() + ?.replace(/\s{2,}/g, ' ') + .replace(/\n/g, ' ') + ?.slice(0, 50) + return (promise = promise.catch(err => { + if (ignoredErrs.includes(err)) return // already caught + log(`${currentQueue()} Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`) + if (!(err instanceof Error)) { + // strange things may happen + err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`) // we should be prepared for them } if (customErrFn) { - return customErrFn(err); + return customErrFn(err) } - }); + })) }, /** @@ -276,15 +293,15 @@ module.exports = { */ throw(err) { - if (ignoredErrs.includes(err)) return promise; // already caught + if (ignoredErrs.includes(err)) return promise // already caught return this.add(`throw error: ${err.message}`, () => { - if (ignoredErrs.includes(err)) return; // already caught - throw err; - }); + if (ignoredErrs.includes(err)) return // already caught + throw err + }) }, ignoreErr(err) { - ignoredErrs.push(err); + ignoredErrs.push(err) }, /** @@ -293,7 +310,7 @@ module.exports = { */ saveFirstAsyncError(err) { if (asyncErr === null) { - asyncErr = err; + asyncErr = err } }, @@ -302,7 +319,7 @@ module.exports = { * @inner */ getAsyncErr() { - return asyncErr; + return asyncErr }, /** @@ -310,7 +327,7 @@ module.exports = { * @inner */ cleanAsyncErr() { - asyncErr = null; + asyncErr = null }, /** @@ -319,9 +336,9 @@ module.exports = { * @inner */ stop() { - debug(this.toString()); - log(`${currentQueue()} Stopping recording promises`); - running = false; + debug(this.toString()) + log(`${currentQueue()} Stopping recording promises`) + running = false }, /** @@ -332,7 +349,7 @@ module.exports = { * @inner */ promise() { - return promise; + return promise }, /** @@ -341,7 +358,7 @@ module.exports = { * @inner */ scheduled() { - return tasks.slice(-MAX_TASKS).join('\n'); + return tasks.slice(-MAX_TASKS).join('\n') }, /** @@ -350,7 +367,7 @@ module.exports = { * @inner */ getQueueId() { - return queueId; + return queueId }, /** @@ -359,21 +376,25 @@ module.exports = { * @inner */ toString() { - return `Queue: ${currentQueue()}\n\nTasks: ${this.scheduled()}`; + return `Queue: ${currentQueue()}\n\nTasks: ${this.scheduled()}` }, - -}; +} function getTimeoutPromise(timeoutMs, taskName) { - let timer; - if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`); - return [new Promise((done, reject) => { - timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9); - }), timer]; + let timer + if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`) + return [ + new Promise((done, reject) => { + timer = setTimeout(() => { + reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)) + }, timeoutMs || 2e9) + }), + timer, + ] } function currentQueue() { - let session = ''; - if (sessionId) session = `<${sessionId}> `; - return `[${queueId}] ${session}`; + let session = '' + if (sessionId) session = `<${sessionId}> ` + return `[${queueId}] ${session}` } diff --git a/package.json b/package.json index 7215933d9..2cbe4fd69 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "figures": "3.2.0", "fn-args": "4.0.0", "fs-extra": "11.2.0", + "fuse.js": "^7.0.0", "glob": "^9", "html-minifier-terser": "7.2.0", "inquirer": "6.5.2", From a95d3af9d38ccc0968d4203bab6ace31556655f5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 00:36:19 +0200 Subject: [PATCH 2/7] fixed test --- test/runner/codecept_test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js index 1759a610c..5fc2b5f20 100644 --- a/test/runner/codecept_test.js +++ b/test/runner/codecept_test.js @@ -170,9 +170,9 @@ describe('CodeceptJS Runner', () => { it('should filter by feature tags', done => { process.chdir(codecept_dir) exec(`${codecept_run_config('codecept.grep.2.js')} --grep @feature_grep --invert`, (err, stdout) => { - stdout.should.not.include('@feature_grep') // feature - stdout.should.not.include('grep message 1') - stdout.should.not.include('grep message 2') + debug(stdout) + stdout.should.include('0 passed') + stdout.should.include('No tests found by pattern: /@feature_grep/') // feature assert(!err) done() }) From 9598783a80fdd0d10973a5ed6be2a6a017aa8e5a Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 04:23:04 +0200 Subject: [PATCH 3/7] updated plugins docs From ed4a07f5164d8ff42db8e424989e6bbe17d12bfa Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 05:54:11 +0200 Subject: [PATCH 4/7] improved unit tests setup --- .github/workflows/test.yml | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae84f5304..6ac6e3d7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,27 +9,42 @@ on: - '**' jobs: - build: + unit-tests: + name: Unit tests + runs-on: ubuntu-22.04 + + strategy: + matrix: + node-version: [20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm i + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: npm run test:unit + runner-tests: + name: Runner tests runs-on: ubuntu-22.04 strategy: matrix: - node-version: [20.x] + node-version: [20.x, 22.x] steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - run: npm i --force - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - uses: nick-fields/retry@v3 - with: - timeout_minutes: 6 - max_attempts: 3 - retry_on: error - command: npm test + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm i + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: npm run test:runner From 68e7d6cabaf8beb406ac9864862a044481bcb329 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 15:38:40 +0200 Subject: [PATCH 5/7] fixed interface test for CI mode --- test/runner/interface_test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js index b81f63727..72d8023bc 100644 --- a/test/runner/interface_test.js +++ b/test/runner/interface_test.js @@ -156,7 +156,12 @@ describe('CodeceptJS Interface', () => { expect(output).toContain('OK') expect(output).toContain('0 passed') expect(output).toContain('2 skipped') - expect(err).toBeFalsy() + if (process.env.CI) { + // we notify that no tests were executed, which is not expected on CI + expect(err).toBeTruthy() + } else { + expect(err).toBeFalsy() + } done() }) }) From 09d39ae4ef9f4fd8b29dd6d7eb9964a0b5466caf Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 21:59:02 +0200 Subject: [PATCH 6/7] fixed on ci --- test/runner/codecept_test.js | 3 ++- test/runner/interface_test.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js index 5fc2b5f20..8141179e2 100644 --- a/test/runner/codecept_test.js +++ b/test/runner/codecept_test.js @@ -173,7 +173,8 @@ describe('CodeceptJS Runner', () => { debug(stdout) stdout.should.include('0 passed') stdout.should.include('No tests found by pattern: /@feature_grep/') // feature - assert(!err) + // fails on CI, but not on local + assert(process.env.CI ? err : !err) done() }) }) diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js index 72d8023bc..daa40b342 100644 --- a/test/runner/interface_test.js +++ b/test/runner/interface_test.js @@ -156,6 +156,7 @@ describe('CodeceptJS Interface', () => { expect(output).toContain('OK') expect(output).toContain('0 passed') expect(output).toContain('2 skipped') + console.log(err) if (process.env.CI) { // we notify that no tests were executed, which is not expected on CI expect(err).toBeTruthy() From 4d167d5b8817fbc470782071492d876cae37c409 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 7 Jan 2025 22:12:06 +0200 Subject: [PATCH 7/7] added env variable config --- lib/listener/emptyRun.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/listener/emptyRun.js b/lib/listener/emptyRun.js index 5b128989f..2fa8af341 100644 --- a/lib/listener/emptyRun.js +++ b/lib/listener/emptyRun.js @@ -47,9 +47,10 @@ module.exports = function () { output.print(output.styles.bold('npx codeceptjs run --debug --grep "' + results[0].item + '"')) } } - if (process.env.CI) { + if (process.env.CI && !process.env.DONT_FAIL_ON_EMPTY_RUN) { output.print() - output.error('No tests were executed. Failing on CI') + output.error('No tests were executed. Failing on CI to avoid false positives') + output.error('To disable this check, set `DONT_FAIL_ON_EMPTY_RUN` environment variable to true in CI config') process.exitCode = 1 } }