From 80c73a90e475c14766a86f1f41461895e3cce7f6 Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Wed, 29 May 2024 10:10:19 +0200 Subject: [PATCH 1/3] fix(cli): stale process --- lib/cli.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 4312a5078..269430287 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -186,20 +186,24 @@ class Cli extends Base { } } - let stack = err.stack ? err.stack.split('\n') : []; - if (stack[0] && stack[0].includes(err.message)) { - stack.shift(); - } + try { + let stack = err.stack ? err.stack.split('\n') : []; + if (stack[0] && stack[0].includes(err.message)) { + stack.shift(); + } - if (output.level() < 3) { - stack = stack.slice(0, 3); - } + if (output.level() < 3) { + stack = stack.slice(0, 3); + } - err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; + err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; - // clone err object so stack trace adjustments won't affect test other reports - test.err = err; - return test; + // clone err object so stack trace adjustments won't affect test other reports + test.err = err; + return test; + } catch (e) { + throw Error(e); + } }); const originalLog = Base.consoleLog; From cbe818afe71b148427d89149c5ae3c314631da78 Mon Sep 17 00:00:00 2001 From: Horsty Date: Fri, 7 Jun 2024 09:13:43 +0200 Subject: [PATCH 2/3] fix: handle throw error inside retryTo promise (#4377) * fix(webApi): error message of dontSeeCookie (#4357) * Fixed error message for dontSeeCookie() * fix(cli): gherkin command init with TypeScript (#4366) * DOC: Update contributor faces * fix(doc): Expect helper is not described correctly (#4370) * fix: Puppeteer helper doc typo (#4369) * chore(deps): bump devtools from 8.36.1 to 8.38.0 (#4374) * chore(deps-dev): bump playwright from 1.44.0 to 1.44.1 (#4376) Bumps [playwright](https://github.com/microsoft/playwright) from 1.44.0 to 1.44.1. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.44.0...v1.44.1) --- updated-dependencies: - dependency-name: playwright dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump axios from 1.6.7 to 1.7.2 (#4372) * chore(deps-dev): bump typedoc-plugin-markdown from 3.17.1 to 4.0.3 (#4373) * fix: handle throw error inside retryTo promise * fix: test due to update on retryTo plugin * fix: add a test case succeed after 2 retry --------- Signed-off-by: dependabot[bot] Co-authored-by: Arthur Stankevich Co-authored-by: Ando NARY <36852616+andonary@users.noreply.github.com> Co-authored-by: GitHub Action Co-authored-by: KobeN <7845001+kobenguyent@users.noreply.github.com> Co-authored-by: Yoann Fleury Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- README.md | 2 +- docs/helpers/{Expect.md => ExpectHelper.md} | 6 +- docs/helpers/Puppeteer.md | 2 +- lib/command/gherkin/init.js | 26 ++++-- lib/command/utils.js | 19 +++- lib/helper/{Expect.js => ExpectHelper.js} | 2 +- lib/helper/Playwright.js | 2 +- lib/helper/Puppeteer.js | 6 +- lib/plugin/retryTo.js | 57 ++++++------ package.json | 8 +- .../codecept.Playwright.coverage.js | 2 +- test/acceptance/codecept.Playwright.js | 2 +- .../acceptance/codecept.Playwright.retryTo.js | 2 +- test/acceptance/codecept.Puppeteer.js | 2 +- test/acceptance/codecept.Testcafe.js | 2 +- .../codecept.WebDriver.devtools.coverage.js | 2 +- .../acceptance/codecept.WebDriver.devtools.js | 2 +- test/acceptance/codecept.WebDriver.js | 2 +- test/acceptance/retryTo_test.js | 12 +++ .../gherkin/config_js/codecept.conf.init.js | 16 ++++ .../gherkin/config_ts/codecept.conf.init.ts | 15 +++ test/helper/Expect_test.js | 2 +- test/plugin/plugin_test.js | 13 +++ test/runner/gherkin_test.js | 93 +++++++++++++++++++ test/unit/plugin/retryto_test.js | 8 +- 25 files changed, 240 insertions(+), 65 deletions(-) rename docs/helpers/{Expect.md => ExpectHelper.md} (98%) rename lib/helper/{Expect.js => ExpectHelper.js} (99%) create mode 100644 test/data/sandbox/configs/gherkin/config_js/codecept.conf.init.js create mode 100644 test/data/sandbox/configs/gherkin/config_ts/codecept.conf.init.ts create mode 100644 test/runner/gherkin_test.js diff --git a/README.md b/README.md index 3417207aa..b483e3c80 100644 --- a/README.md +++ b/README.md @@ -315,8 +315,8 @@ Thanks all to those who are and will have contributing to this awesome project! - + diff --git a/docs/helpers/Expect.md b/docs/helpers/ExpectHelper.md similarity index 98% rename from docs/helpers/Expect.md rename to docs/helpers/ExpectHelper.md index 4628ea41c..7ffe415ab 100644 --- a/docs/helpers/Expect.md +++ b/docs/helpers/ExpectHelper.md @@ -1,8 +1,8 @@ --- -permalink: /helpers/Expect +permalink: /helpers/ExpectHelper editLink: false sidebar: auto -title: Expect +title: ExpectHelper --- @@ -20,7 +20,7 @@ Zero-configuration when paired with other helpers like REST, Playwright: { helpers: { Playwright: {...}, - Expect: {}, + ExpectHelper: {}, } } ``` diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md index 3bf4cf9a0..55fba6b8c 100644 --- a/docs/helpers/Puppeteer.md +++ b/docs/helpers/Puppeteer.md @@ -2476,7 +2476,7 @@ Returns **void** automatically synchronized promise through #recorder [18]: https://codecept.io/helpers/FileSystem -[19]: https://pptr.dev/next/guides/request-interception +[19]: https://pptr.dev/guides/network-interception [20]: https://github.com/GoogleChrome/puppeteer/issues/1313 diff --git a/lib/command/gherkin/init.js b/lib/command/gherkin/init.js index b0aabe4a8..7ac70231c 100644 --- a/lib/command/gherkin/init.js +++ b/lib/command/gherkin/init.js @@ -4,7 +4,7 @@ const mkdirp = require('mkdirp'); const output = require('../../output'); const { fileExists } = require('../../utils'); const { - getConfig, getTestRoot, updateConfig, safeFileWrite, + getConfig, getTestRoot, updateConfig, safeFileWrite, findConfigFile, } = require('../utils'); const featureFile = `Feature: Business rules @@ -26,7 +26,17 @@ Given('I have a defined step', () => { module.exports = function (genPath) { const testsPath = getTestRoot(genPath); + const configFile = findConfigFile(testsPath); + + if (!configFile) { + output.error( + "Can't initialize Gherkin. This command must be run in an already initialized project." + ); + process.exit(1); + } + const config = getConfig(testsPath); + const extension = path.extname(configFile).substring(1); output.print('Initializing Gherkin (Cucumber BDD) for CodeceptJS'); output.print('--------------------------'); @@ -53,18 +63,18 @@ module.exports = function (genPath) { output.success(`Created ${dir}, place step definitions into it`); } - if (safeFileWrite(path.join(dir, 'steps.js'), stepsFile)) { - output.success('Created sample steps file: step_definitions/steps.js'); + if (safeFileWrite(path.join(dir, `steps.${extension}`), stepsFile)) { + output.success( + `Created sample steps file: step_definitions/steps.${extension}` + ); } config.gherkin = { - features: './features/*.feature', - steps: [ - './step_definitions/steps.js', - ], + features: "./features/*.feature", + steps: [`./step_definitions/steps.${extension}`], }; - updateConfig(testsPath, config); + updateConfig(testsPath, config, extension); output.success('Gherkin setup is done.'); output.success('Start writing feature files and implement corresponding steps.'); diff --git a/lib/command/utils.js b/lib/command/utils.js index ac3cf7fd6..b8c9ca2fa 100644 --- a/lib/command/utils.js +++ b/lib/command/utils.js @@ -41,15 +41,15 @@ function fail(msg) { module.exports.fail = fail; -function updateConfig(testsPath, config, key, extension = 'js') { +function updateConfig(testsPath, config, extension) { const configFile = path.join(testsPath, `codecept.conf.${extension}`); if (!fileExists(configFile)) { - console.log(); const msg = `codecept.conf.${extension} config can\'t be updated automatically`; + console.log(); console.log(`${output.colors.bold.red(msg)}`); - console.log('Please update it manually:'); + console.log(`${output.colors.bold.red("Please update it manually:")}`); console.log(); - console.log(`${key}: ${config[key]}`); + console.log(config); console.log(); return; } @@ -104,3 +104,14 @@ module.exports.createOutputDir = (config, testRoot) => { mkdirp.sync(outputDir); } }; + +module.exports.findConfigFile = (testsPath) => { + const extensions = ['js', 'ts']; + for (const ext of extensions) { + const configFile = path.join(testsPath, `codecept.conf.${ext}`); + if (fileExists(configFile)) { + return configFile; + } + } + return null; +} diff --git a/lib/helper/Expect.js b/lib/helper/ExpectHelper.js similarity index 99% rename from lib/helper/Expect.js rename to lib/helper/ExpectHelper.js index c9de3693c..c6a9dd27d 100644 --- a/lib/helper/Expect.js +++ b/lib/helper/ExpectHelper.js @@ -23,7 +23,7 @@ import('chai').then(chai => { *{ * helpers: { * Playwright: {...}, - * Expect: {}, + * ExpectHelper: {}, * } *} * ``` diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 697e27561..d03e3af2b 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1981,7 +1981,7 @@ class Playwright extends Helper { */ async dontSeeCookie(name) { const cookies = await this.browserContext.cookies(); - empty(`cookie ${name} to be set`).assert(cookies.filter(c => c.name === name)); + empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name)); } /** diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 2bb8edac8..2ae747dea 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -1627,7 +1627,7 @@ class Puppeteer extends Helper { */ async dontSeeCookie(name) { const cookies = await this.page.cookies(); - empty(`cookie ${name} to be set`).assert(cookies.filter(c => c.name === name)); + empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name)); } /** @@ -2472,12 +2472,12 @@ class Puppeteer extends Helper { } /** - * Mocks network request using [`Request Interception`](https://pptr.dev/next/guides/request-interception) + * Mocks network request using [`Request Interception`](https://pptr.dev/guides/network-interception) * * ```js * I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort()); * ``` - * This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/next/guides/request-interception) + * This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/guides/network-interception) * * @param {string|RegExp} [url] URL, regex or pattern for to match URL * @param {function} [handler] a function to process request diff --git a/lib/plugin/retryTo.js b/lib/plugin/retryTo.js index 9929944f1..211b39ac7 100644 --- a/lib/plugin/retryTo.js +++ b/lib/plugin/retryTo.js @@ -1,5 +1,4 @@ const recorder = require('../recorder'); -const store = require('../store'); const { debug } = require('../output'); const defaultConfig = { @@ -73,49 +72,55 @@ const defaultConfig = { * const retryTo = codeceptjs.container.plugins('retryTo'); * ``` * -*/ + */ module.exports = function (config) { config = Object.assign(defaultConfig, config); + function retryTo(callback, maxTries, pollInterval = config.pollInterval) { + return new Promise((done, reject) => { + let tries = 1; - if (config.registerGlobal) { - global.retryTo = retryTo; - } - return retryTo; + function handleRetryException(err) { + recorder.throw(err); + reject(err); + } - function retryTo(callback, maxTries, pollInterval = undefined) { - let tries = 1; - if (!pollInterval) pollInterval = config.pollInterval; - - let err = null; - - return new Promise((done) => { const tryBlock = async () => { + tries++; recorder.session.start(`retryTo ${tries}`); - await callback(tries); + try { + await callback(tries); + } catch (err) { + handleRetryException(err); + } + + // Call done if no errors recorder.add(() => { recorder.session.restore(`retryTo ${tries}`); done(null); }); - recorder.session.catch((e) => { - err = e; + + // Catch errors and retry + recorder.session.catch((err) => { recorder.session.restore(`retryTo ${tries}`); - tries++; if (tries <= maxTries) { debug(`Error ${err}... Retrying`); - err = null; - - recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval)); + recorder.add(`retryTo ${tries}`, () => + setTimeout(tryBlock, pollInterval) + ); } else { - done(null); + // if maxTries reached + handleRetryException(err); } }); }; - recorder.add('retryTo', async () => { - tryBlock(); - }); - }).then(() => { - if (err) recorder.throw(err); + recorder.add('retryTo', tryBlock); }); } + + if (config.registerGlobal) { + global.retryTo = retryTo; + } + + return retryTo; }; diff --git a/package.json b/package.json index 93eba907c..2c56b255b 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@xmldom/xmldom": "0.8.10", "acorn": "8.11.3", "arrify": "2.0.1", - "axios": "1.6.7", + "axios": "1.7.2", "chai": "5.1.1", "chai-deep-match": "1.2.1", "chai-exclude": "2.1.0", @@ -89,7 +89,7 @@ "cross-spawn": "7.0.3", "css-to-xpath": "0.1.0", "csstoxpath": "1.6.0", - "devtools": "8.36.1", + "devtools": "8.38.0", "envinfo": "7.11.1", "escape-string-regexp": "4.0.0", "figures": "3.2.0", @@ -148,7 +148,7 @@ "jsdoc": "4.0.3", "jsdoc-typeof-plugin": "1.0.0", "json-server": "0.10.1", - "playwright": "1.44.0", + "playwright": "1.44.1", "puppeteer": "22.10.0", "qrcode-terminal": "0.12.0", "rosie": "2.1.1", @@ -161,7 +161,7 @@ "tsd": "^0.31.0", "tsd-jsdoc": "2.5.0", "typedoc": "0.25.13", - "typedoc-plugin-markdown": "3.17.1", + "typedoc-plugin-markdown": "4.0.3", "typescript": "5.3.3", "wdio-docker-service": "1.5.0", "webdriverio": "8.36.1", diff --git a/test/acceptance/codecept.Playwright.coverage.js b/test/acceptance/codecept.Playwright.coverage.js index f1c57c214..11017f986 100644 --- a/test/acceptance/codecept.Playwright.coverage.js +++ b/test/acceptance/codecept.Playwright.coverage.js @@ -23,7 +23,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: 'test/acceptance/output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: false, diff --git a/test/acceptance/codecept.Playwright.js b/test/acceptance/codecept.Playwright.js index d45a49f50..e4dca9ded 100644 --- a/test/acceptance/codecept.Playwright.js +++ b/test/acceptance/codecept.Playwright.js @@ -23,7 +23,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: 'test/acceptance/output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: false, diff --git a/test/acceptance/codecept.Playwright.retryTo.js b/test/acceptance/codecept.Playwright.retryTo.js index d45a49f50..e4dca9ded 100644 --- a/test/acceptance/codecept.Playwright.retryTo.js +++ b/test/acceptance/codecept.Playwright.retryTo.js @@ -23,7 +23,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: 'test/acceptance/output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: false, diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js index d18087620..8c2a8e987 100644 --- a/test/acceptance/codecept.Puppeteer.js +++ b/test/acceptance/codecept.Puppeteer.js @@ -19,7 +19,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: './output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: false, diff --git a/test/acceptance/codecept.Testcafe.js b/test/acceptance/codecept.Testcafe.js index 1191bb18a..13b529103 100644 --- a/test/acceptance/codecept.Testcafe.js +++ b/test/acceptance/codecept.Testcafe.js @@ -9,7 +9,7 @@ module.exports.config = { url: TestHelper.siteUrl(), show: true, }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: false, diff --git a/test/acceptance/codecept.WebDriver.devtools.coverage.js b/test/acceptance/codecept.WebDriver.devtools.coverage.js index d4f1089cf..6fb8f5aaa 100644 --- a/test/acceptance/codecept.WebDriver.devtools.coverage.js +++ b/test/acceptance/codecept.WebDriver.devtools.coverage.js @@ -21,7 +21,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: './output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, mocha: {}, diff --git a/test/acceptance/codecept.WebDriver.devtools.js b/test/acceptance/codecept.WebDriver.devtools.js index fc749d22e..f86312789 100644 --- a/test/acceptance/codecept.WebDriver.devtools.js +++ b/test/acceptance/codecept.WebDriver.devtools.js @@ -21,7 +21,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: './output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: async () => new Promise(done => { diff --git a/test/acceptance/codecept.WebDriver.js b/test/acceptance/codecept.WebDriver.js index fd7af8b22..54dd4e3e4 100644 --- a/test/acceptance/codecept.WebDriver.js +++ b/test/acceptance/codecept.WebDriver.js @@ -21,7 +21,7 @@ module.exports.config = { require: '../support/ScreenshotSessionHelper.js', outputPath: './output', }, - Expect: {}, + ExpectHelper: {}, }, include: {}, bootstrap: async () => new Promise(done => { diff --git a/test/acceptance/retryTo_test.js b/test/acceptance/retryTo_test.js index b568e8607..9d0e1f631 100644 --- a/test/acceptance/retryTo_test.js +++ b/test/acceptance/retryTo_test.js @@ -14,3 +14,15 @@ Scenario('retryTo works with non await steps @plugin', async () => { if (tryNum < 3) I.waitForVisible('.nothing', 1); }, 4); }); + +Scenario('Should fail after reached max retries', async () => { + await retryTo(() => { + throw new Error('Custom pluginRetryTo Error'); + }, 3); +}); + +Scenario('Should succeed at the third attempt @plugin', async () => { + await retryTo(async (tryNum) => { + if (tryNum < 2) throw new Error('Custom pluginRetryTo Error'); + }, 3); +}); \ No newline at end of file diff --git a/test/data/sandbox/configs/gherkin/config_js/codecept.conf.init.js b/test/data/sandbox/configs/gherkin/config_js/codecept.conf.init.js new file mode 100644 index 000000000..61e56f1dd --- /dev/null +++ b/test/data/sandbox/configs/gherkin/config_js/codecept.conf.init.js @@ -0,0 +1,16 @@ +/** @type {CodeceptJS.MainConfig} */ +exports.config = { + tests: "./*_test.js", + output: "./output", + helpers: { + Playwright: { + browser: "chromium", + url: "http://localhost", + show: true, + }, + }, + include: { + I: "./steps_file.js", + }, + name: "CodeceptJS", +}; diff --git a/test/data/sandbox/configs/gherkin/config_ts/codecept.conf.init.ts b/test/data/sandbox/configs/gherkin/config_ts/codecept.conf.init.ts new file mode 100644 index 000000000..86fe45f4f --- /dev/null +++ b/test/data/sandbox/configs/gherkin/config_ts/codecept.conf.init.ts @@ -0,0 +1,15 @@ +export const config: CodeceptJS.MainConfig = { + tests: "./*_test.ts", + output: "./output", + helpers: { + Playwright: { + browser: "chromium", + url: "http://localhost", + show: true + } + }, + include: { + I: "./steps_file" + }, + name: "CodeceptJS" +} diff --git a/test/helper/Expect_test.js b/test/helper/Expect_test.js index 9567943c0..7db470e01 100644 --- a/test/helper/Expect_test.js +++ b/test/helper/Expect_test.js @@ -5,7 +5,7 @@ import('chai').then(chai => { expect = chai.expect; }); -const ExpectHelper = require('../../lib/helper/Expect'); +const ExpectHelper = require('../../lib/helper/ExpectHelper'); global.codeceptjs = require('../../lib'); diff --git a/test/plugin/plugin_test.js b/test/plugin/plugin_test.js index 2f615d1d5..1185967b4 100644 --- a/test/plugin/plugin_test.js +++ b/test/plugin/plugin_test.js @@ -61,4 +61,17 @@ describe('CodeceptJS plugin', function () { done(); }); }); + + it('should retry to failure', (done) => { + exec( + `${config_run_config('codecept.Playwright.retryTo.js', 'Should fail after reached max retries')} --verbose`, (err, stdout) => { + const lines = stdout.split('\n'); + expect(lines).toEqual( + expect.arrayContaining([expect.stringContaining('Custom pluginRetryTo Error')]) + ); + expect(err).toBeTruthy(); + done(); + } + ); + }); }); diff --git a/test/runner/gherkin_test.js b/test/runner/gherkin_test.js new file mode 100644 index 000000000..7a1dbd94a --- /dev/null +++ b/test/runner/gherkin_test.js @@ -0,0 +1,93 @@ +const assert = require("assert"); +const path = require("path"); +const fs = require("fs"); +const exec = require("child_process").exec; + +const runner = path.join(__dirname, "/../../bin/codecept.js"); +const codecept_dir = path.join(__dirname, "/../data/sandbox/configs/gherkin/"); + +describe("gherkin bdd commands", () => { + describe("bdd:init", () => { + let codecept_dir_js = path.join(codecept_dir, "config_js"); + let codecept_dir_ts = path.join(codecept_dir, "config_ts"); + + beforeEach(() => { + fs.copyFileSync( + path.join(codecept_dir_js, "codecept.conf.init.js"), + path.join(codecept_dir_js, "codecept.conf.js") + ); + fs.copyFileSync( + path.join(codecept_dir_ts, "codecept.conf.init.ts"), + path.join(codecept_dir_ts, "codecept.conf.ts") + ); + }); + + afterEach(() => { + try { + fs.rmSync(path.join(codecept_dir_js, "codecept.conf.js")); + fs.rmSync(path.join(codecept_dir_js, "features"), { + recursive: true, + }); + fs.rmSync(path.join(codecept_dir_js, "step_definitions"), { + recursive: true, + }); + } catch (e) {} + try { + fs.rmSync(path.join(codecept_dir_ts, "codecept.conf.ts")); + fs.rmSync(path.join(codecept_dir_ts, "features"), { + recursive: true, + }); + fs.rmSync(path.join(codecept_dir_ts, "step_definitions"), { + recursive: true, + }); + } catch (e) {} + }); + + [ + { + codecept_dir_test: codecept_dir_js, + extension: "js", + }, + { + codecept_dir_test: codecept_dir_ts, + extension: "ts", + }, + ].forEach(({ codecept_dir_test, extension }) => { + it(`prepare CodeceptJS to run feature files (codecept.conf.${extension})`, (done) => { + exec(`${runner} gherkin:init ${codecept_dir_test}`, (err, stdout) => { + let dir = path.join(codecept_dir_test, "features"); + + stdout.should.include( + "Initializing Gherkin (Cucumber BDD) for CodeceptJS" + ); + stdout.should.include( + `Created ${dir}, place your *.feature files in it` + ); + stdout.should.include( + "Created sample feature file: features/basic.feature" + ); + + dir = path.join(codecept_dir_test, "step_definitions"); + stdout.should.include( + `Created ${dir}, place step definitions into it` + ); + stdout.should.include( + `Created sample steps file: step_definitions/steps.${extension}` + ); + assert(!err); + + const configResult = fs + .readFileSync( + path.join(codecept_dir_test, `codecept.conf.${extension}`) + ) + .toString(); + configResult.should.contain(`features: './features/*.feature'`); + configResult.should.contain( + `steps: ['./step_definitions/steps.${extension}']` + ); + done(); + }); + }); + }); + }); +}); diff --git a/test/unit/plugin/retryto_test.js b/test/unit/plugin/retryto_test.js index 28187e02c..293adf1d7 100644 --- a/test/unit/plugin/retryto_test.js +++ b/test/unit/plugin/retryto_test.js @@ -20,11 +20,11 @@ describe('retryTo plugin', () => { it('should execute few times command on fail', async () => { let counter = 0; let errorCaught = false; - await retryTo(() => { - recorder.add(() => counter++); - recorder.add(() => { throw new Error('Ups'); }); - }, 5, 10); try { + await retryTo(() => { + recorder.add(() => counter++); + recorder.add(() => { throw new Error('Ups'); }); + }, 5, 10); await recorder.promise(); } catch (err) { errorCaught = true; From 71771791d77cae5902abd7168ce4757503e7b6dc Mon Sep 17 00:00:00 2001 From: Horsty Date: Fri, 7 Jun 2024 11:17:30 +0200 Subject: [PATCH 3/3] Ensure scenario termination (#4378) * chore: Add test:stale script for running state_test.js * fix: handle async function better to prevent stale the process * fix: done() call multiple times * chore: rename files and test description * fix: playwright error on test * fix: stale process due to retryTo error handler * fix: lint previous code * fix: error due to the previous merge * fix: handle async function better to prevent stale the process * fix: lint previous code --------- Co-authored-by: KobeN <7845001+kobenguyent@users.noreply.github.com> --- lib/command/gherkin/init.js | 2 +- lib/plugin/retryTo.js | 9 +- lib/scenario.js | 101 ++++++++++--------- test/acceptance/retryTo_test.js | 10 +- test/data/sandbox/codecept.scenario-stale.js | 10 ++ test/data/sandbox/test.scenario-stale.js | 22 ++++ test/plugin/plugin_test.js | 27 +++-- test/runner/run_workers_test.js | 1 - test/runner/scenario_stale_test.js | 22 ++++ test/runner/timeout_test.js | 2 +- 10 files changed, 143 insertions(+), 63 deletions(-) create mode 100644 test/data/sandbox/codecept.scenario-stale.js create mode 100644 test/data/sandbox/test.scenario-stale.js create mode 100644 test/runner/scenario_stale_test.js diff --git a/lib/command/gherkin/init.js b/lib/command/gherkin/init.js index a909c27bb..bcb418dc9 100644 --- a/lib/command/gherkin/init.js +++ b/lib/command/gherkin/init.js @@ -70,7 +70,7 @@ module.exports = function (genPath) { } config.gherkin = { - features: './features/*.feature', + features: "./features/*.feature", steps: [`./step_definitions/steps.${extension}`], }; diff --git a/lib/plugin/retryTo.js b/lib/plugin/retryTo.js index ce89e91ee..4a2940176 100644 --- a/lib/plugin/retryTo.js +++ b/lib/plugin/retryTo.js @@ -104,7 +104,9 @@ module.exports = function (config) { recorder.session.restore(`retryTo ${tries}`); if (tries <= maxTries) { debug(`Error ${err}... Retrying`); - recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval)); + recorder.add(`retryTo ${tries}`, () => + setTimeout(tryBlock, pollInterval) + ); } else { // if maxTries reached handleRetryException(err); @@ -112,7 +114,10 @@ module.exports = function (config) { }); }; - recorder.add('retryTo', tryBlock); + recorder.add('retryTo', tryBlock).catch(err => { + console.error('An error occurred:', err); + done(null); + }); }); } diff --git a/lib/scenario.js b/lib/scenario.js index 40f5759a1..ab816a1ab 100644 --- a/lib/scenario.js +++ b/lib/scenario.js @@ -18,6 +18,16 @@ const injectHook = function (inject, suite) { return recorder.promise(); }; +function makeDoneCallableOnce(done) { + let called = false; + return function (err) { + if (called) { + return; + } + called = true; + return done(err); + }; +} /** * Wraps test function, injects support objects from container, * starts promise chain with recorder, performs before/after hooks @@ -34,15 +44,17 @@ module.exports.test = (test) => { test.async = true; test.fn = function (done) { + const doneFn = makeDoneCallableOnce(done); recorder.errHandler((err) => { recorder.session.start('teardown'); recorder.cleanAsyncErr(); - if (test.throws) { // check that test should actually fail + if (test.throws) { + // check that test should actually fail try { assertThrown(err, test.throws); event.emit(event.test.passed, test); event.emit(event.test.finished, test); - recorder.add(() => done()); + recorder.add(doneFn); return; } catch (newErr) { err = newErr; @@ -50,40 +62,26 @@ module.exports.test = (test) => { } event.emit(event.test.failed, test, err); event.emit(event.test.finished, test); - recorder.add(() => done(err)); + recorder.add(() => doneFn(err)); }); if (isAsyncFunction(testFn)) { event.emit(event.test.started, test); - - const catchError = e => { - recorder.throw(e); - recorder.catch((e) => { - const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr(); - recorder.session.start('teardown'); - recorder.cleanAsyncErr(); - event.emit(event.test.failed, test, err); - event.emit(event.test.finished, test); - recorder.add(() => done(err)); + testFn + .call(test, getInjectedArguments(testFn, test)) + .then(() => { + recorder.add('fire test.passed', () => { + event.emit(event.test.passed, test); + event.emit(event.test.finished, test); + }); + recorder.add('finish test', doneFn); + }) + .catch((err) => { + recorder.throw(err); + }) + .finally(() => { + recorder.catch(); }); - }; - - let injectedArguments; - try { - injectedArguments = getInjectedArguments(testFn, test); - } catch (e) { - catchError(e); - return; - } - - testFn.call(test, injectedArguments).then(() => { - recorder.add('fire test.passed', () => { - event.emit(event.test.passed, test); - event.emit(event.test.finished, test); - }); - recorder.add('finish test', () => done()); - recorder.catch(); - }).catch(catchError); return; } @@ -97,7 +95,7 @@ module.exports.test = (test) => { event.emit(event.test.passed, test); event.emit(event.test.finished, test); }); - recorder.add('finish test', () => done()); + recorder.add('finish test', doneFn); recorder.catch(); } }; @@ -109,13 +107,14 @@ module.exports.test = (test) => { */ module.exports.injected = function (fn, suite, hookName) { return function (done) { + const doneFn = makeDoneCallableOnce(done); const errHandler = (err) => { recorder.session.start('teardown'); recorder.cleanAsyncErr(); event.emit(event.test.failed, suite, err); if (hookName === 'after') event.emit(event.test.after, suite); if (hookName === 'afterSuite') event.emit(event.suite.after, suite); - recorder.add(() => done(err)); + recorder.add(() => doneFn(err)); }; recorder.errHandler((err) => { @@ -137,28 +136,32 @@ module.exports.injected = function (fn, suite, hookName) { const opts = suite.opts || {}; const retries = opts[`retry${ucfirst(hookName)}`] || 0; - promiseRetry(async (retry, number) => { - try { - recorder.startUnlessRunning(); - await fn.call(this, getInjectedArguments(fn)); - await recorder.promise().catch(err => retry(err)); - } catch (err) { - retry(err); - } finally { - if (number < retries) { - recorder.stop(); - recorder.start(); + promiseRetry( + async (retry, number) => { + try { + recorder.startUnlessRunning(); + await fn.call(this, getInjectedArguments(fn)); + await recorder.promise().catch((err) => retry(err)); + } catch (err) { + retry(err); + } finally { + if (number < retries) { + recorder.stop(); + recorder.start(); + } } - } - }, { retries }) + }, + { retries }, + ) .then(() => { recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite)); - recorder.add(`finish ${hookName} hook`, () => done()); + recorder.add(`finish ${hookName} hook`, doneFn); recorder.catch(); - }).catch((e) => { + }) + .catch((e) => { recorder.throw(e); recorder.catch((e) => { - const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr(); + const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr(); errHandler(err); }); recorder.add('fire hook.failed', () => event.emit(event.hook.failed, suite, e)); diff --git a/test/acceptance/retryTo_test.js b/test/acceptance/retryTo_test.js index 5ae9bbff2..c3bfe5a74 100644 --- a/test/acceptance/retryTo_test.js +++ b/test/acceptance/retryTo_test.js @@ -15,6 +15,14 @@ Scenario('retryTo works with non await steps @plugin', async () => { }, 4); }); +Scenario('Should be succeed', async ({ I }) => { + I.amOnPage('http://example.org'); + I.waitForVisible('.nothing', 1); // should fail here but it won't terminate + await retryTo((tryNum) => { + I.see('.doesNotMatter'); + }, 10); +}); + Scenario('Should fail after reached max retries', async () => { await retryTo(() => { throw new Error('Custom pluginRetryTo Error'); @@ -25,4 +33,4 @@ Scenario('Should succeed at the third attempt @plugin', async () => { await retryTo(async (tryNum) => { if (tryNum < 2) throw new Error('Custom pluginRetryTo Error'); }, 3); -}); +}); \ No newline at end of file diff --git a/test/data/sandbox/codecept.scenario-stale.js b/test/data/sandbox/codecept.scenario-stale.js new file mode 100644 index 000000000..f07399b38 --- /dev/null +++ b/test/data/sandbox/codecept.scenario-stale.js @@ -0,0 +1,10 @@ +exports.config = { + tests: './test.scenario-stale.js', + timeout: 10000, + retry: 2, + output: './output', + include: {}, + bootstrap: false, + mocha: {}, + name: 'sandbox', +}; diff --git a/test/data/sandbox/test.scenario-stale.js b/test/data/sandbox/test.scenario-stale.js new file mode 100644 index 000000000..30ec3c9b0 --- /dev/null +++ b/test/data/sandbox/test.scenario-stale.js @@ -0,0 +1,22 @@ +Feature('Scenario should not be staling'); + +const SHOULD_NOT_STALE = 'should not stale scenario error'; + +Scenario('Rejected promise should not stale the process', async () => { + await new Promise((_resolve, reject) => setTimeout(reject(new Error(SHOULD_NOT_STALE)), 500)); +}); + +Scenario('Should handle throw inside synchronous and terminate gracefully', () => { + throw new Error(SHOULD_NOT_STALE); +}); +Scenario('Should handle throw inside async and terminate gracefully', async () => { + throw new Error(SHOULD_NOT_STALE); +}); + +Scenario('Should throw, retry and keep failing', async () => { + setTimeout(() => { + throw new Error(SHOULD_NOT_STALE); + }, 500); + await new Promise((resolve) => setTimeout(resolve, 300)); + throw new Error(SHOULD_NOT_STALE); +}).retry(2); diff --git a/test/plugin/plugin_test.js b/test/plugin/plugin_test.js index ade9edbb6..0f5e6f0a7 100644 --- a/test/plugin/plugin_test.js +++ b/test/plugin/plugin_test.js @@ -32,6 +32,15 @@ describe('CodeceptJS plugin', function () { }); }); + it('should failed before the retryTo instruction', (done) => { + exec(`${config_run_config('codecept.Playwright.retryTo.js', 'Should be succeed')} --verbose`, (err, stdout) => { + expect(stdout).toContain('locator.waitFor: Timeout 1000ms exceeded.'), + expect(stdout).toContain('[1] Error | Error: element (.nothing) still not visible after 1 sec'), + expect(err).toBeTruthy(); + done(); + }); + }); + it('should generate the coverage report', (done) => { exec(`${config_run_config('codecept.Playwright.coverage.js', '@coverage')} --debug`, (err, stdout) => { const lines = stdout.split('\n'); @@ -63,13 +72,15 @@ describe('CodeceptJS plugin', function () { }); it('should retry to failure', (done) => { - exec(`${config_run_config('codecept.Playwright.retryTo.js', 'Should fail after reached max retries')} --verbose`, (err, stdout) => { - const lines = stdout.split('\n'); - expect(lines).toEqual( - expect.arrayContaining([expect.stringContaining('Custom pluginRetryTo Error')]), - ); - expect(err).toBeTruthy(); - done(); - }); + exec( + `${config_run_config('codecept.Playwright.retryTo.js', 'Should fail after reached max retries')} --verbose`, (err, stdout) => { + const lines = stdout.split('\n'); + expect(lines).toEqual( + expect.arrayContaining([expect.stringContaining('Custom pluginRetryTo Error')]) + ); + expect(err).toBeTruthy(); + done(); + } + ); }); }); diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js index 0cd0f3b46..ccd1afe99 100644 --- a/test/runner/run_workers_test.js +++ b/test/runner/run_workers_test.js @@ -19,7 +19,6 @@ describe('CodeceptJS Workers Runner', function () { if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); console.log(`${codecept_run} 3 --debug`); exec(`${codecept_run} 3 --debug`, (err, stdout) => { - console.log('aaaaaaaaaaaaa', stdout); expect(stdout).toContain('CodeceptJS'); // feature expect(stdout).toContain('glob current dir'); expect(stdout).toContain('From worker @1_grep print message 1'); diff --git a/test/runner/scenario_stale_test.js b/test/runner/scenario_stale_test.js new file mode 100644 index 000000000..927d92acd --- /dev/null +++ b/test/runner/scenario_stale_test.js @@ -0,0 +1,22 @@ +const { expect } = require('expect'); +const path = require('path'); +const { exec } = require('child_process'); + +const runner = path.join(__dirname, '/../../bin/codecept.js'); +const codecept_dir = path.join(__dirname, '/../data/sandbox'); +const codecept_run = `${runner} run`; +const config_run_config = config => `${codecept_run} --config ${codecept_dir}/${config}`; + +describe('Scenario termination check', () => { + before(() => { + process.chdir(codecept_dir); + }); + + it('Should always fail and terminate', (done) => { + exec(config_run_config('codecept.scenario-stale.js'), (err, stdout) => { + expect(stdout).toContain('should not stale scenario error'); // feature + expect(err).toBeTruthy(); + done(); + }); + }); +}); diff --git a/test/runner/timeout_test.js b/test/runner/timeout_test.js index 79d74f054..8e14efc09 100644 --- a/test/runner/timeout_test.js +++ b/test/runner/timeout_test.js @@ -11,7 +11,7 @@ describe('CodeceptJS Timeouts', function () { it('should stop test when timeout exceeded', (done) => { exec(config_run_config('codecept.conf.js', 'timed out'), (err, stdout) => { - console.log(stdout); + debug_this_test && console.log(stdout); expect(stdout).toContain('Timeout 2s exceeded'); expect(stdout).toContain('Timeout 1s exceeded'); expect(err).toBeTruthy();