From 9ee7da7d849cb99a3b987e6d710a7cb074431456 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Fri, 23 Dec 2022 14:13:43 +0100 Subject: [PATCH 01/30] Update CONTRIBUTING.md (#3545) --- .github/CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0c8e3a7c6..ebd538a27 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,6 +2,8 @@ Thanks for getting here. If you have a good will to improve CodeceptJS we are always glad to help. Ask questions, raise issues, ping in Twitter. +Go over the steps in [this](https://github.com/firstcontributions/first-contributions) guide as first contributions + To start you need: 1. Fork and clone the repo. From eb4d29330e7eb3837047b389d16c55357510be6e Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Fri, 23 Dec 2022 14:21:36 +0100 Subject: [PATCH 02/30] fix: bump node version to 14.x (#3550) --- .github/workflows/dtslint.yml | 2 +- .github/workflows/puppeteer.yml | 2 +- .github/workflows/webdriver.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dtslint.yml b/.github/workflows/dtslint.yml index 9532c72d2..1a4268826 100644 --- a/.github/workflows/dtslint.yml +++ b/.github/workflows/dtslint.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - uses: actions/checkout@v1 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.github/workflows/puppeteer.yml b/.github/workflows/puppeteer.yml index c6e3d195d..f0e8e330c 100644 --- a/.github/workflows/puppeteer.yml +++ b/.github/workflows/puppeteer.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/webdriver.yml b/.github/workflows/webdriver.yml index 36e8f6c89..5fba8046f 100644 --- a/.github/workflows/webdriver.yml +++ b/.github/workflows/webdriver.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - run: docker run -d --net=host --shm-size=2g selenium/standalone-chrome:3.141.59-oxygen From 63eb2a6779e210a8f72f32070008f25818f50f71 Mon Sep 17 00:00:00 2001 From: KaledinaDARIA <93767566+KaledinaDARIA@users.noreply.github.com> Date: Fri, 23 Dec 2022 20:30:59 +0300 Subject: [PATCH 03/30] fix bag 3551 (#3552) --- lib/helper/Playwright.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index d07220b1f..a4cd4050d 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -2981,7 +2981,7 @@ async function refreshContextSession() { async function saveVideoForPage(page, name) { if (!page.video()) return null; - const fileName = `${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name).slice(0, 245)}.webm`; + const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${Date.now()}_${clearString(name)}`.slice(0, 245)}.webm`; page.video().saveAs(fileName).then(() => { if (!page) return; page.video().delete().catch(e => {}); From b2e9950528828ee3f7dbee4f8f27bc190da7a6a2 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Fri, 23 Dec 2022 18:37:33 +0100 Subject: [PATCH 04/30] fix: optional params of some ApiDataFactory functions (#3523) --- docs/helpers/ApiDataFactory.md | 8 ++++---- lib/helper/ApiDataFactory.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/helpers/ApiDataFactory.md b/docs/helpers/ApiDataFactory.md index 4e4d24ef7..2a43f43ef 100644 --- a/docs/helpers/ApiDataFactory.md +++ b/docs/helpers/ApiDataFactory.md @@ -228,8 +228,8 @@ I.have('user', { }, { age: 33, height: 55 }) #### Parameters - `factory` **any** factory to use -- `params` **any** predefined parameters -- `options` **any** options for programmatically generate the attributes +- `params` **any?** predefined parameters +- `options` **any?** options for programmatically generate the attributes Returns **[Promise][5]<any>** @@ -252,8 +252,8 @@ I.haveMultiple('post', 3, { author: 'davert' }, { publish_date: '01.01.1997' }); - `factory` **any** - `times` **any** -- `params` **any** -- `options` **any** +- `params` **any?** +- `options` **any?** [1]: https://github.com/rosiejs/rosie diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index bdf8c7bf6..ce172bcca 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -262,8 +262,8 @@ class ApiDataFactory extends Helper { * ``` * * @param {*} factory factory to use - * @param {*} params predefined parameters - * @param {*} options options for programmatically generate the attributes + * @param {*} [params] predefined parameters + * @param {*} [options] options for programmatically generate the attributes * @returns {Promise<*>} */ have(factory, params, options) { @@ -288,8 +288,8 @@ class ApiDataFactory extends Helper { * * @param {*} factory * @param {*} times - * @param {*} params - * @param {*} options + * @param {*} [params] + * @param {*} [options] */ haveMultiple(factory, times, params, options) { const promises = []; From 69384f38bcb476ae3dd842abc10cf0108a4bd1e6 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Fri, 23 Dec 2022 18:46:14 +0100 Subject: [PATCH 05/30] bump testcafe version (#3521) * bump testcafe version * fix: UTs --- lib/helper/TestCafe.js | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js index fe5252d82..02e6c7c73 100644 --- a/lib/helper/TestCafe.js +++ b/lib/helper/TestCafe.js @@ -545,8 +545,8 @@ class TestCafe extends Helper { // TODO As far as I understand the testcafe docs this should do a multi-select // but it does not work - const clickOpts = { ctrl: option.length > 1 }; - await this.t.click(el, clickOpts).catch(mapError); + // const clickOpts = { ctrl: option.length > 1 }; + await this.t.click(el).catch(mapError); for (const key of option) { const opt = key; @@ -555,7 +555,7 @@ class TestCafe extends Helper { try { optEl = el.child('option').withText(opt); if (await optEl.count) { - await this.t.click(optEl, clickOpts).catch(mapError); + await this.t.click(optEl).catch(mapError); continue; } // eslint-disable-next-line no-empty @@ -566,7 +566,7 @@ class TestCafe extends Helper { const sel = `[value="${opt}"]`; optEl = el.find(sel); if (await optEl.count) { - await this.t.click(optEl, clickOpts).catch(mapError); + await this.t.click(optEl).catch(mapError); } // eslint-disable-next-line no-empty } catch (err) { diff --git a/package.json b/package.json index 952aba338..37b54fa6a 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "runok": "^0.9.2", "sinon": "^9.2.4", "sinon-chai": "^3.7.0", - "testcafe": "^1.18.3", + "testcafe": "^2.1.0", "ts-morph": "^3.1.3", "ts-node": "^10.9.1", "tsd-jsdoc": "https://github.com/englercj/tsd-jsdoc.git", From 06db6635943f4ff8d80a76428dc4c7b4ecb74879 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Fri, 23 Dec 2022 18:48:08 +0100 Subject: [PATCH 06/30] fix: bump faker version (#3509) * fix: bump faker version * fixed typos * fix: UTs * fix: UTs * update docs and failed tests Co-authored-by: davert --- docs/best.md | 2 +- docs/helpers/ApiDataFactory.md | 4 ++-- docs/helpers/GraphQLDataFactory.md | 4 ++-- docs/index.md | 2 +- lib/helper/ApiDataFactory.js | 4 ++-- lib/helper/GraphQLDataFactory.js | 4 ++-- lib/plugin/fakerTransform.js | 4 ++-- package.json | 2 +- test/data/graphql/users_factory.js | 4 ++-- test/data/rest/posts_factory.js | 4 ++-- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/best.md b/docs/best.md index ca7d468c8..bdcde6542 100644 --- a/docs/best.md +++ b/docs/best.md @@ -215,7 +215,7 @@ include: { * When you need to customize access to API and go beyond what ApiDataFactory provides, implement DAO: ```js -const faker = require('@faker-js/faker'); +const { faker } = require('@faker-js/faker'); const { I } = inject(); const { output } = require('codeceptjs'); diff --git a/docs/helpers/ApiDataFactory.md b/docs/helpers/ApiDataFactory.md index 2a43f43ef..2e25d1636 100644 --- a/docs/helpers/ApiDataFactory.md +++ b/docs/helpers/ApiDataFactory.md @@ -53,8 +53,8 @@ See the example for Posts factories: ```js // tests/factories/posts.js -const Factory = require('rosie').Factory; -const faker = require('@faker-js/faker'); +const { Factory } = require('rosie'); +const { faker } = require('@faker-js/faker'); module.exports = new Factory() // no need to set id, it will be set by REST API diff --git a/docs/helpers/GraphQLDataFactory.md b/docs/helpers/GraphQLDataFactory.md index 0e0698138..d7795e773 100644 --- a/docs/helpers/GraphQLDataFactory.md +++ b/docs/helpers/GraphQLDataFactory.md @@ -53,8 +53,8 @@ See the example for Users factories: ```js // tests/factories/users.js -const Factory = require('rosie').Factory; -const faker = require('@faker-js/faker'); +const { Factory } = require('rosie').Factory; +const { faker } = require('@faker-js/faker'); // Used with a constructor function passed to Factory, so that the final build // object matches the necessary pattern to be sent as the variables object. diff --git a/docs/index.md b/docs/index.md index 1cc491581..308a63f70 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,7 +86,7 @@ Scenario('Checkout test', ({ I }) => { Can we use it for long scenarios? Sure! ```js -const faker = require('@faker-js/faker'); // Use 3rd-party JS code +const { faker } = require('@faker-js/faker'); // Use 3rd-party JS code Feature('Store'); diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index ce172bcca..24014fbb5 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -46,8 +46,8 @@ const REST = require('./REST'); * ```js * // tests/factories/posts.js * - * const Factory = require('rosie').Factory; - * const faker = require('@faker-js/faker'); + * const { Factory } = require('rosie'); + * const { faker } = require('@faker-js/faker'); * * module.exports = new Factory() * // no need to set id, it will be set by REST API diff --git a/lib/helper/GraphQLDataFactory.js b/lib/helper/GraphQLDataFactory.js index 47b182fa6..8ae1b0c98 100644 --- a/lib/helper/GraphQLDataFactory.js +++ b/lib/helper/GraphQLDataFactory.js @@ -46,8 +46,8 @@ const GraphQL = require('./GraphQL'); * ```js * // tests/factories/users.js * - * const Factory = require('rosie').Factory; - * const faker = require('@faker-js/faker'); + * const { Factory } = require('rosie').Factory; + * const { faker } = require('@faker-js/faker'); * * // Used with a constructor function passed to Factory, so that the final build * // object matches the necessary pattern to be sent as the variables object. diff --git a/lib/plugin/fakerTransform.js b/lib/plugin/fakerTransform.js index b33bac5a8..d2c08cf2a 100644 --- a/lib/plugin/fakerTransform.js +++ b/lib/plugin/fakerTransform.js @@ -1,4 +1,4 @@ -const faker = require('@faker-js/faker'); +const { faker } = require('@faker-js/faker'); const transform = require('../transform'); /** @@ -44,7 +44,7 @@ const transform = require('../transform'); module.exports = function (config) { transform.addTransformerBeforeAll('gherkin.examples', (value) => { if (typeof value === 'string' && value.length > 0) { - return faker.fake(value); + return faker.helpers.fake(value); } return value; }); diff --git a/package.json b/package.json index 37b54fa6a..3dd054bf1 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "devDependencies": { "@codeceptjs/detox-helper": "^1.0.2", "@codeceptjs/mock-request": "^0.3.1", - "@faker-js/faker": "^5.5.3", + "@faker-js/faker": "^7.6.0", "@pollyjs/adapter-puppeteer": "^5.1.0", "@pollyjs/core": "^5.1.0", "@types/inquirer": "^0.0.35", diff --git a/test/data/graphql/users_factory.js b/test/data/graphql/users_factory.js index 4caae0313..ec12f1385 100644 --- a/test/data/graphql/users_factory.js +++ b/test/data/graphql/users_factory.js @@ -1,5 +1,5 @@ -const Factory = require('rosie').Factory; -const faker = require('@faker-js/faker'); +const { Factory } = require('rosie'); +const { faker } = require('@faker-js/faker'); module.exports = new Factory(function (buildObject) { this.input = { ...buildObject }; diff --git a/test/data/rest/posts_factory.js b/test/data/rest/posts_factory.js index aa5cf8b66..c06402ecd 100644 --- a/test/data/rest/posts_factory.js +++ b/test/data/rest/posts_factory.js @@ -1,5 +1,5 @@ -const Factory = require('rosie').Factory; -const faker = require('@faker-js/faker'); +const { Factory } = require('rosie'); +const { faker } = require('@faker-js/faker'); module.exports = new Factory() .attr('author', () => faker.name.findName()) From 986c5420a623e926a171429cfeaf1ce2ae8eef9c Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Tue, 31 Jan 2023 00:40:28 +0100 Subject: [PATCH 07/30] clean up (#3513) --- README.md | 60 ++++++++++++++++++++++---------------------- lib/helper/Appium.js | 4 +++ lib/recorder.js | 2 +- package.json | 1 - 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 197119f17..61f87392b 100644 --- a/README.md +++ b/README.md @@ -294,36 +294,36 @@ When using Typescript, replace `module.exports` with `export` for autocompletion Thanks all to those who are and will have contributing to this awesome project! [//]: contributor-faces - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [//]: contributor-faces diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js index 2ac0a9fdf..650addcc3 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -131,13 +131,17 @@ class Appium extends Webdriver { * @augments WebDriver */ + // @ts-ignore constructor(config) { super(config); this.isRunning = false; this.axios = axios.create(); + this.options = undefined; + this.config = undefined; webdriverio = require('webdriverio'); + // @ts-ignore (!webdriverio.VERSION || webdriverio.VERSION.indexOf('4') !== 0) ? wdioV4 = false : wdioV4 = true; } diff --git a/lib/recorder.js b/lib/recorder.js index 5d0bd668c..9cf17ff4d 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -127,7 +127,7 @@ module.exports = { }, /** - * @param {string} name + * @param {string} [name] * @inner */ restore(name) { diff --git a/package.json b/package.json index 3dd054bf1..6111763a3 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "jsdoc": "^3.6.10", "jsdoc-typeof-plugin": "^1.0.0", "json-server": "^0.10.1", - "mocha-parallel-tests": "^2.3.0", "nightmare": "^3.0.2", "nodemon": "^1.19.4", "playwright": "^1.23.2", From dc5e81ff5e180d0e7a9b8ff6fd644617d2d3baa1 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Tue, 31 Jan 2023 00:41:55 +0100 Subject: [PATCH 08/30] fix: handle the case ts config is not loaded with codeceptjs-ui (#3548) --- lib/config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index 9404eb5db..3eaa49e55 100644 --- a/lib/config.js +++ b/lib/config.js @@ -79,7 +79,11 @@ class Config { configFile = path.resolve(configFile || '.'); if (!fileExists(configFile)) { - throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`); + configFile = configFile.replace('.js', '.ts'); + + if (!fileExists(configFile)) { + throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`); + } } // is config file From 9d1175805df13b91d096df2de1732186465824b4 Mon Sep 17 00:00:00 2001 From: Vladimir Semenov <20096510+vovsemenv@users.noreply.github.com> Date: Tue, 31 Jan 2023 02:44:58 +0300 Subject: [PATCH 09/30] feature(allure): Add framework and language labels to tests (#3539) * add framework labels * tests * DRY --- lib/plugin/allure.js | 8 ++++++++ test/runner/allure_test.js | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js index d12c37d3c..16b9cfae3 100644 --- a/lib/plugin/allure.js +++ b/lib/plugin/allure.js @@ -99,6 +99,7 @@ module.exports = (config) => { reporter.pendingCase = function (testName, timestamp, opts = {}) { reporter.startCase(testName, timestamp); + plugin.addCommonMetadata(); if (opts.description) plugin.setDescription(opts.description); if (opts.severity) plugin.severity(opts.severity); if (opts.severity) plugin.addLabel('tag', opts.severity); @@ -190,6 +191,11 @@ module.exports = (config) => { } }; + plugin.addCommonMetadata = () => { + plugin.addLabel('language', 'javascript'); + plugin.addLabel('framework', 'codeceptjs'); + }; + event.dispatcher.on(event.suite.before, (suite) => { reporter.startSuite(suite.fullTitle()); }); @@ -208,6 +214,7 @@ module.exports = (config) => { event.dispatcher.on(event.test.before, (test) => { reporter.startCase(test.title); + plugin.addCommonMetadata(); if (config.enableScreenshotDiffPlugin) { const currentTest = reporter.getCurrentTest(); currentTest.addLabel('testType', 'screenshotDiff'); @@ -235,6 +242,7 @@ module.exports = (config) => { } else { // this means before suite failed, we should report this. reporter.startCase(`BeforeSuite of suite ${reporter.getCurrentSuite().name} failed.`); + plugin.addCommonMetadata(); reporter.endCase('failed', err); } }); diff --git a/test/runner/allure_test.js b/test/runner/allure_test.js index 9546ddc9a..cf95659ab 100644 --- a/test/runner/allure_test.js +++ b/test/runner/allure_test.js @@ -45,6 +45,11 @@ describe('CodeceptJS Allure Plugin', function () { expect(nestedMetaStep.name[0]).toEqual('I am in path "."'); expect(testCase.steps[0].step[0].steps.length).toEqual(1); + expect(testCase.labels[0].label).toEqual([ + { $: { name: 'language', value: 'javascript' } }, + { $: { name: 'framework', value: 'codeceptjs' } }, + ]); + const secondMetaStep = testCase.steps[0].step[1]; expect(secondMetaStep.name[0]).toEqual('I see file "allure.conf.js"'); }); From f561a6a628e2880df00d75c5d211a6af93f57dad Mon Sep 17 00:00:00 2001 From: Valentin Date: Sat, 4 Feb 2023 07:22:17 +0100 Subject: [PATCH 10/30] docs(typescript): fix typo (#3575) --- docs/typescript.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript.md b/docs/typescript.md index 758ccab94..ef1c6861a 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -41,7 +41,7 @@ Then select TypeScript as the first question: Then a config file and new tests will be created in TypeScript format. -If a config file is set in TypeScript format (`codecept.conf.ts`) package `node-ts` will be used to run tests. +If a config file is set in TypeScript format (`codecept.conf.ts`) package `ts-node` will be used to run tests. ## Promise-Based Typings From f1523f65d9dd470565367dbdfdf13e081af30db3 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Thu, 9 Feb 2023 15:49:35 +0100 Subject: [PATCH 11/30] fix: stabilize the init tests (#3582) --- test/runner/init_test.js | 57 +++++----------------------------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/test/runner/init_test.js b/test/runner/init_test.js index eef75f43a..4cfcc106a 100644 --- a/test/runner/init_test.js +++ b/test/runner/init_test.js @@ -6,61 +6,16 @@ const runner = path.join(__dirname, '../../bin/codecept.js'); describe('Init Command', function () { this.timeout(20000); - // this.retries(4); - it('init - welcome message', async () => { - const result = await run([runner, 'init'], []); + it('steps are showing', async () => { + const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, 'y']); result.should.include('Welcome to CodeceptJS initialization tool'); result.should.include('It will prepare and configure a test environment for you'); result.should.include('Installing to'); - }); - - it('init - Do you plan to write tests in TypeScript?', async () => { - const result = await run([runner, 'init'], []); result.should.include('? Do you plan to write tests in TypeScript? (y/N)'); - }); - - it('init - TypeScript and Promise Based', async () => { - const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, 'y']); - result.should.include('? Do you plan to write tests in TypeScript? (y/N)'); - result.should.include('promise-based typings'); - result.should.include('(y/N) y'); - }); - - it('init - Where are your tests located?', async () => { - const result = await run([runner, 'init'], ['Y']); - result.should.include('? Where are your tests located? (./*_test.ts)'); - }); - - it('init - What helpers do you want to use? (Use arrow keys)?', async () => { - const result = await run([runner, 'init'], ['Y', ENTER, ENTER]); - result.should.include('? What helpers do you want to use? (Use arrow keys)'); - for (const item of ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe']) { - result.should.include(item); - } - result.should.include('(Move up and down to reveal more choices)'); - }); - - it('init - Where should logs, screenshots, and reports to be stored? (./output)', async () => { - const result = await run([runner, 'init'], [ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER]); - result.should.include('? What helpers do you want to use? REST'); - result.should.include('Where should logs, screenshots, and reports to be stored? (./output)'); - }); - - it('init - Do you want to enable localization for tests?', async () => { - const result = await run([runner, 'init'], [ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, ENTER]); - result.should.include('? Do you want to enable localization for tests?'); - result.should.include('❯ English (no localization)'); - for (const item of ['de-DE', 'it-IT', 'fr-FR', 'ja-JP', 'pl-PL', 'pt-BR']) { - result.should.include(item); - } - result.should.include('(Move up and down to reveal more choices)'); - }); - - it('init - [REST] Endpoint of API you are going to test (http://localhost:3000/api)', async () => { - const result = await run([runner, 'init'], [ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, ENTER, ENTER]); - result.should.include('Do you want to enable localization for tests? http://bit.ly/3GNUBbh Eng'); - result.should.include('Configure helpers...'); - result.should.include('? [REST] Endpoint of API you are going to test (http://localhost:3000/api)'); + result.should.include('Where are your tests located? ./*_test.ts'); + result.should.include('What helpers do you want to use? REST'); + result.should.include('? Do you want to use JSONResponse helper for assertions on JSON responses?'); + result.should.include('? Would you prefer to use promise-based typings for all I.* commands'); }); }); From eba53145210ac592f2a35c5531991af047d7168c Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Sat, 11 Feb 2023 03:38:31 +0100 Subject: [PATCH 12/30] fix(types): missing def for PW config (#3581) --- docs/helpers/Playwright.md | 1 + lib/helper/Playwright.js | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index d84914ea2..d368cdd58 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -51,6 +51,7 @@ Type: [object][5] - `video` **[boolean][26]?** enables video recording for failed tests; videos are saved into `output/videos` folder - `keepVideoForPassedTests` **[boolean][26]?** save videos for passed tests; videos are saved into `output/videos` folder - `trace` **[boolean][26]?** record [tracing information][35] with screenshots and snapshots. +- `keepTraceForPassedTests` **[boolean][26]?** save trace for passed tests. - `fullPageScreenshots` **[boolean][26]?** make full page screenshots on failure. - `uniqueScreenshotNames` **[boolean][26]?** option to prevent screenshot override if you have scenarios with the same name in different suites. - `keepBrowserState` **[boolean][26]?** keep browser state between tests when `restart` is set to 'session'. diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index a4cd4050d..6f9b1f514 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -66,6 +66,7 @@ const pathSeparator = path.sep; * @prop {boolean} [video=false] - enables video recording for failed tests; videos are saved into `output/videos` folder * @prop {boolean} [keepVideoForPassedTests=false] - save videos for passed tests; videos are saved into `output/videos` folder * @prop {boolean} [trace=false] - record [tracing information](https://playwright.dev/docs/trace-viewer) with screenshots and snapshots. + * @prop {boolean} [keepTraceForPassedTests=false] - save trace for passed tests. * @prop {boolean} [fullPageScreenshots=false] - make full page screenshots on failure. * @prop {boolean} [uniqueScreenshotNames=false] - option to prevent screenshot override if you have scenarios with the same name in different suites. * @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to 'session'. From 7df51f5ae9eccefaafad9db6be0604c2c2cee7eb Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Sun, 12 Feb 2023 22:32:23 +0100 Subject: [PATCH 13/30] docs: Update README.md (#3579) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 61f87392b..c741e5332 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,7 @@ npx codeceptjs def . Later you can even automagically update Type Definitions to include your own custom [helpers methods](docs/helpers.md). Note: -- CodeceptJS requires Node.js version `8.9.1+` or later. -- To use the parallel tests execution, requiring Node.js version `11.7` or later. +- CodeceptJS requires Node.js version `12+` or later. ## Usage From 6e8e171d7c52a125d236dbf32d872136c1df77f8 Mon Sep 17 00:00:00 2001 From: Egor Bodnar <63167966+EgorBodnar@users.noreply.github.com> Date: Mon, 13 Feb 2023 05:33:25 +0800 Subject: [PATCH 14/30] feat(allure): screen diff block from allure 2 (#3573) Co-authored-by: Michael Bodnarchuk --- docs/plugins.md | 204 +++++++++++++++++++++++++++++++++---------- lib/plugin/allure.js | 73 ++++++++++++++++ 2 files changed, 229 insertions(+), 48 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 890906f5c..1d363e584 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -7,11 +7,72 @@ title: Plugins +## addAttachment + +Add an attachment to the current test case + +### Parameters + +- `name` **[string][1]** Name of the attachment +- `buffer` **[Buffer][2]** Buffer of the attachment +- `type` **[string][1]** MIME type of the attachment + +## addLabel + +Adds a label with the given name and value to the current test in the Allure report + +### Parameters + +- `name` **[string][1]** name of the label to add +- `value` **[string][1]** value of the label to add + +## addParameter + +Adds a parameter with the given kind, name, and value to the current test in the Allure report + +### Parameters + +- `kind` **[string][1]** kind of the parameter to add +- `name` **[string][1]** name of the parameter to add +- `value` **[string][1]** value of the parameter to add + +## addScreenDiff + +Add a special screen diff block to the current test case + +### Parameters + +- `name` **[string][1]** Name of the screen diff block +- `expectedImg` **[string][1]** string representing the contents of the expected image file encoded in base64 +- `actualImg` **[string][1]** string representing the contents of the actual image file encoded in base64 +- `diffImg` **[string][1]** string representing the contents of the diff image file encoded in base64. + Could be generated by image comparison lib like "pixelmatch" or alternative + +## createStep + +A method for creating a step in a test case. + +### Parameters + +- `name` **[string][1]** The name of the step. +- `stepFunc` **[Function][3]** The function that should be executed for this step. (optional, default `()=>{}`) + +Returns **any** The result of the step function. + +## setDescription + +Set description for the current test case + +### Parameters + +- `description` **[string][1]** Description for the test case +- `type` **[string][1]** MIME type of the description (optional, default `'text/plain'`) + ## allure Allure reporter -![][1] +![][4] Enables Allure reporter. @@ -43,7 +104,7 @@ Launch Allure server and see the report like on a screenshot above: - `outputDir` - a directory where allure reports should be stored. Standard output directory is set by default. - `enableScreenshotDiffPlugin` - a boolean flag for add screenshot diff to report. To attach, tou need to attach three files to the report - "diff.png", "actual.png", "expected.png". - See [Allure Screenshot Plugin][2] + See [Allure Screenshot Plugin][5] #### Public API @@ -71,7 +132,20 @@ const allure = codeceptjs.container.plugins('allure'); }); ``` -![Created Step Image][3] +![Created Step Image][6] + +- `addScreenDiff(name, expectedImg, actualImg, diffImg)` - add a special screen diff block to the current test case + image must be a string representing the contents of the expected image file encoded in base64 + Example of usage: + +```js +const expectedImg = fs.readFileSync('expectedImg.png', { encoding: 'base64' }); +... +allure.addScreenDiff('Screen Diff', expectedImg, actualImg, diffImg); +``` + +![Overlay][7] +![Diff][8] - `severity(value)` - adds severity label - `epic(value)` - adds epic label @@ -84,6 +158,16 @@ const allure = codeceptjs.container.plugins('allure'); - `config` +## allure + +Creates an instance of the allure reporter + +### Parameters + +- `config` **Config** Configuration for the allure reporter (optional, default `{outputDir:global.output_dir}`) + +Returns **[Object][9]** Instance of the allure reporter + ## autoDelay Sometimes it takes some time for a page to respond to user's actions. @@ -449,7 +533,7 @@ Possible config options: ## customLocator -Creates a [custom locator][4] by using special attributes in HTML. +Creates a [custom locator][10] by using special attributes in HTML. If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests, you can enable this plugin to simplify matching elements with these attributes: @@ -599,9 +683,9 @@ This method works with WebDriver, Playwright, Puppeteer, Appium helpers. Function parameter `el` represents a matched element. Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list: -- [Playwright ElementHandle][5] -- [Puppeteer][6] -- [webdriverio element][7] +- [Playwright ElementHandle][11] +- [Puppeteer][12] +- [webdriverio element][13] #### Configuration @@ -615,17 +699,17 @@ const eachElement = codeceptjs.container.plugins('eachElement'); ### Parameters -- `purpose` **[string][8]** +- `purpose` **[string][1]** - `locator` **CodeceptJS.LocatorOrString** -- `fn` **[Function][9]** +- `fn` **[Function][3]** -Returns **([Promise][10]<any> | [undefined][11])** +Returns **([Promise][14]<any> | [undefined][15])** ## fakerTransform -Use the [faker.js][12] package to generate fake data inside examples on your gherkin tests +Use the [faker.js][16] package to generate fake data inside examples on your gherkin tests -![Faker.js][13] +![Faker.js][17] #### Usage @@ -663,7 +747,7 @@ Scenario Outline: ... ## pauseOnFail -Automatically launches [interactive pause][14] when a test fails. +Automatically launches [interactive pause][18] when a test fails. Useful for debugging flaky tests on local environment. Add this plugin to config file: @@ -679,6 +763,20 @@ Enable it manually on each run via `-p` option: npx codeceptjs run -p pauseOnFail +## reporter + +Type: Allure + +### pendingCase + +Mark a test case as pending + +#### Parameters + +- `testName` **[string][1]** Name of the test case +- `timestamp` **[number][19]** Timestamp of the test case +- `opts` **[Object][9]** Options for the test case (optional, default `{}`) + ## retryFailedStep Retries each failed step in a test. @@ -846,14 +944,14 @@ Possible config options: ## selenoid -[Selenoid][15] plugin automatically starts browsers and video recording. +[Selenoid][20] plugin automatically starts browsers and video recording. Works with WebDriver helper. ### Prerequisite This plugin **requires Docker** to be installed. -> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][16] tool from Selenoid +> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][21] tool from Selenoid ### Usage @@ -882,7 +980,7 @@ plugins: { } ``` -When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][17] and start Selenoid automatically. +When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][22] and start Selenoid automatically. It will also create `browsers.json` file required by Selenoid. In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file. @@ -894,10 +992,10 @@ In automatic mode the latest version of browser will be used for tests. It is re While this plugin can create containers for you for better control it is recommended to create and launch containers manually. This is especially useful for Continous Integration server as you can configure scaling for Selenoid containers. -> Use [Selenoid Configuration Manager][16] to create and start containers semi-automatically. +> Use [Selenoid Configuration Manager][21] to create and start containers semi-automatically. 1. Create `browsers.json` file in the same directory `codecept.conf.js` is located - [Refer to Selenoid documentation][18] to know more about browsers.json. + [Refer to Selenoid documentation][23] to know more about browsers.json. _Sample browsers.json_ @@ -922,7 +1020,7 @@ _Sample browsers.json_ 2. Create Selenoid container -Run the following command to create a container. To know more [refer here][19] +Run the following command to create a container. To know more [refer here][24] ```bash docker create \ @@ -955,7 +1053,7 @@ When `allure` plugin is enabled a video is attached to report automatically. | enableVideo | Enable video recording and use `video` folder of output (default: false) | | enableLog | Enable log recording and use `logs` folder of output (default: false) | | deletePassed | Delete video and logs of passed tests (default : true) | -| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][20] to know more | +| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][25] to know more | ### Parameters @@ -963,7 +1061,7 @@ When `allure` plugin is enabled a video is attached to report automatically. ## stepByStepReport -![step-by-step-report][21] +![step-by-step-report][26] Generates step by step report for a test. After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow. @@ -1144,7 +1242,7 @@ This plugin allows to run webdriverio services like: - browserstack - appium -A complete list of all available services can be found on [webdriverio website][22]. +A complete list of all available services can be found on [webdriverio website][27]. #### Setup @@ -1156,7 +1254,7 @@ See examples below: #### Selenium Standalone Service -Install `@wdio/selenium-standalone-service` package, as [described here][23]. +Install `@wdio/selenium-standalone-service` package, as [described here][28]. It is important to make sure it is compatible with current webdriverio version. Enable `wdio` plugin in plugins list and add `selenium-standalone` service: @@ -1175,7 +1273,7 @@ Please note, this service can be used with Protractor helper as well! #### Sauce Service -Install `@wdio/sauce-service` package, as [described here][24]. +Install `@wdio/sauce-service` package, as [described here][29]. It is important to make sure it is compatible with current webdriverio version. Enable `wdio` plugin in plugins list and add `sauce` service: @@ -1205,50 +1303,60 @@ In the same manner additional services from webdriverio can be installed, enable - `config` -[1]: https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png +[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + +[2]: https://nodejs.org/api/buffer.html + +[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function + +[4]: https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png + +[5]: https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md + +[6]: https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png -[2]: https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md +[7]: https://user-images.githubusercontent.com/63167966/215404458-9a325668-819e-4289-9b42-5807c49ebddb.png -[3]: https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png +[8]: https://user-images.githubusercontent.com/63167966/215404645-73b09da0-9e6d-4352-a123-80c22f7014cd.png -[4]: https://codecept.io/locators#custom-locators +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[5]: https://playwright.dev/docs/api/class-elementhandle +[10]: https://codecept.io/locators#custom-locators -[6]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle +[11]: https://playwright.dev/docs/api/class-elementhandle -[7]: https://webdriver.io/docs/api +[12]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[13]: https://webdriver.io/docs/api -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function +[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise +[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined +[16]: https://www.npmjs.com/package/faker -[12]: https://www.npmjs.com/package/faker +[17]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png -[13]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png +[18]: /basics/#pause -[14]: /basics/#pause +[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[15]: https://aerokube.com/selenoid/ +[20]: https://aerokube.com/selenoid/ -[16]: https://aerokube.com/cm/latest/ +[21]: https://aerokube.com/cm/latest/ -[17]: https://hub.docker.com/u/selenoid +[22]: https://hub.docker.com/u/selenoid -[18]: https://aerokube.com/selenoid/latest/#_prepare_configuration +[23]: https://aerokube.com/selenoid/latest/#_prepare_configuration -[19]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container +[24]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container -[20]: https://docs.docker.com/engine/reference/commandline/create/ +[25]: https://docs.docker.com/engine/reference/commandline/create/ -[21]: https://codecept.io/img/codeceptjs-slideshow.gif +[26]: https://codecept.io/img/codeceptjs-slideshow.gif -[22]: https://webdriver.io +[27]: https://webdriver.io -[23]: https://webdriver.io/docs/selenium-standalone-service.html +[28]: https://webdriver.io/docs/selenium-standalone-service.html -[24]: https://webdriver.io/docs/sauce-service.html +[29]: https://webdriver.io/docs/sauce-service.html diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js index 16b9cfae3..cf80d2157 100644 --- a/lib/plugin/allure.js +++ b/lib/plugin/allure.js @@ -76,6 +76,18 @@ const defaultConfig = { * }); * ``` * ![Created Step Image](https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png) + * + * * `addScreenDiff(name, expectedImg, actualImg, diffImg)` - add a special screen diff block to the current test case + * image must be a string representing the contents of the expected image file encoded in base64 + * Example of usage: + * ```js + * const expectedImg = fs.readFileSync('expectedImg.png', { encoding: 'base64' }); + * ... + * allure.addScreenDiff('Screen Diff', expectedImg, actualImg, diffImg); + * ``` + * ![Overlay](https://user-images.githubusercontent.com/63167966/215404458-9a325668-819e-4289-9b42-5807c49ebddb.png) + * ![Diff](https://user-images.githubusercontent.com/63167966/215404645-73b09da0-9e6d-4352-a123-80c22f7014cd.png) + * * * `severity(value)` - adds severity label * * `epic(value)` - adds epic label * * `feature(value)` - adds feature label @@ -84,18 +96,33 @@ const defaultConfig = { * * `setDescription(description, type)` - sets a description * */ + +/** + * Creates an instance of the allure reporter + * @param {Config} [config={ outputDir: global.output_dir }] - Configuration for the allure reporter + * @returns {Object} Instance of the allure reporter + */ module.exports = (config) => { defaultConfig.outputDir = global.output_dir; config = Object.assign(defaultConfig, config); const plugin = {}; + /** + * @type {Allure} + */ const reporter = new Allure(); reporter.setOptions({ targetDir: config.outputDir }); let currentMetaStep = []; let currentStep; + /** + * Mark a test case as pending + * @param {string} testName - Name of the test case + * @param {number} timestamp - Timestamp of the test case + * @param {Object} [opts={}] - Options for the test case + */ reporter.pendingCase = function (testName, timestamp, opts = {}) { reporter.startCase(testName, timestamp); @@ -107,10 +134,21 @@ module.exports = (config) => { reporter.endCase('pending', { message: opts.message || 'Test ignored' }, timestamp); }; + /** + * Add an attachment to the current test case + * @param {string} name - Name of the attachment + * @param {Buffer} buffer - Buffer of the attachment + * @param {string} type - MIME type of the attachment + */ plugin.addAttachment = (name, buffer, type) => { reporter.addAttachment(name, buffer, type); }; + /** + Set description for the current test case + @param {string} description - Description for the test case + @param {string} [type='text/plain'] - MIME type of the description + */ plugin.setDescription = (description, type) => { const currentTest = reporter.getCurrentTest(); if (currentTest) { @@ -121,6 +159,12 @@ module.exports = (config) => { } }; + /** + A method for creating a step in a test case. + @param {string} name - The name of the step. + @param {Function} [stepFunc=() => {}] - The function that should be executed for this step. + @returns {any} - The result of the step function. + */ plugin.createStep = (name, stepFunc = () => { }) => { let result; let status = 'passed'; @@ -171,6 +215,11 @@ module.exports = (config) => { plugin.addLabel('issue', issue); }; + /** + Adds a label with the given name and value to the current test in the Allure report + @param {string} name - name of the label to add + @param {string} value - value of the label to add + */ plugin.addLabel = (name, value) => { const currentTest = reporter.getCurrentTest(); if (currentTest) { @@ -181,6 +230,12 @@ module.exports = (config) => { } }; + /** + Adds a parameter with the given kind, name, and value to the current test in the Allure report + @param {string} kind - kind of the parameter to add + @param {string} name - name of the parameter to add + @param {string} value - value of the parameter to add + */ plugin.addParameter = (kind, name, value) => { const currentTest = reporter.getCurrentTest(); if (currentTest) { @@ -191,6 +246,24 @@ module.exports = (config) => { } }; + /** + * Add a special screen diff block to the current test case + * @param {string} name - Name of the screen diff block + * @param {string} expectedImg - string representing the contents of the expected image file encoded in base64 + * @param {string} actualImg - string representing the contents of the actual image file encoded in base64 + * @param {string} diffImg - string representing the contents of the diff image file encoded in base64. + * Could be generated by image comparison lib like "pixelmatch" or alternative + */ + plugin.addScreenDiff = (name, expectedImg, actualImg, diffImg) => { + const screenDiff = { + name, + expected: `data:image/png;base64,${expectedImg}`, + actual: `data:image/png;base64,${actualImg}`, + diff: `data:image/png;base64,${diffImg}`, + }; + reporter.addAttachment(name, JSON.stringify(screenDiff), 'application/vnd.allure.image.diff'); + }; + plugin.addCommonMetadata = () => { plugin.addLabel('language', 'javascript'); plugin.addLabel('framework', 'codeceptjs'); From 22bbf28b2541da5834f18bda5b1c817d3cb53f83 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Sun, 12 Feb 2023 22:40:15 +0100 Subject: [PATCH 15/30] fix: cannot load wdio helper (#3578) * fix: cannot load wdio helper * chore: remove wdio5 leftover * chore: remove isWebDriver5() leftover --- lib/helper/WebDriver.js | 193 ++++++---------------------------------- 1 file changed, 29 insertions(+), 164 deletions(-) diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 3ffd76943..1a1e0ca2e 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -8,8 +8,8 @@ const Helper = require('../helper'); const stringIncludes = require('../assert/include').includes; const { urlEquals, equals } = require('../assert/equal'); const { debug } = require('../output'); -const empty = require('../assert/empty').empty; -const truth = require('../assert/truth').truth; +const { empty } = require('../assert/empty'); +const { truth } = require('../assert/truth'); const { xpathLocator, fileExists, @@ -31,8 +31,6 @@ const Locator = require('../locator'); const SHADOW = 'shadow'; const webRoot = 'body'; -let version; - /** * ## Configuration * @@ -266,7 +264,7 @@ const config = {}; * ```js * plugins: { * wdio: { - * enabled: true, + * enabled: true, * services: ['sauce'], * user: ... ,// saucelabs username * key: ... // saucelabs api key @@ -294,7 +292,7 @@ const config = {}; * ```js * plugins: { * wdio: { - * enabled: true, + * enabled: true, * services: ['browserstack'], * user: ... ,// browserstack username * key: ... // browserstack api key @@ -386,16 +384,6 @@ class WebDriver extends Helper { super(config); webdriverio = require('webdriverio'); - try { - version = JSON.parse(fs.readFileSync(path.join(require.resolve('webdriverio'), '/../../', 'package.json')).toString()).version; - } catch (err) { - this.debug('Can\'t detect webdriverio version, assuming webdriverio v6 is used'); - } - - if (isWebDriver5()) { - console.log('DEPRECATION NOTICE:'); - console.log('You are using webdriverio v5. It is recommended to update to webdriverio@6.\nSupport of webdriverio v5 is deprecated and will be removed in CodeceptJS 3.0\n'); - } // set defaults this.root = webRoot; this.isWeb = true; @@ -661,23 +649,23 @@ class WebDriver extends Helper { } /** - * Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test. - * - * First argument is a description of an action. - * Second argument is async function that gets this helper as parameter. - * - * { [`browser`](https://webdriver.io/docs/api.html)) } object from WebDriver API is available. - * - * ```js - * I.useWebDriverTo('open multiple windows', async ({ browser }) { - * // create new window - * await browser.newWindow('https://webdriver.io'); - * }); - * ``` - * - * @param {string} description used to show in logs. - * @param {function} fn async functuion that executed with WebDriver helper as argument - */ + * Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test. + * + * First argument is a description of an action. + * Second argument is async function that gets this helper as parameter. + * + * { [`browser`](https://webdriver.io/docs/api.html)) } object from WebDriver API is available. + * + * ```js + * I.useWebDriverTo('open multiple windows', async ({ browser }) { + * // create new window + * await browser.newWindow('https://webdriver.io'); + * }); + * ``` + * + * @param {string} description used to show in logs. + * @param {function} fn async functuion that executed with WebDriver helper as argument + */ useWebDriverTo(description, fn) { return this._useTo(...arguments); } @@ -1654,7 +1642,6 @@ class WebDriver extends Helper { const res = await this._locate(withStrictLocator(locator), true); assertElementExists(res, locator); const elem = usingFirstElement(res); - if (isWebDriver5()) return elem.moveTo(xOffset, yOffset); return elem.moveTo({ xOffset, yOffset }); } @@ -2043,19 +2030,7 @@ class WebDriver extends Helper { */ async waitForEnabled(locator, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this.$$(withStrictLocator(locator)); - if (!res || res.length === 0) { - return false; - } - const selected = await forEachAsync(res, async el => this.browser.isElementEnabled(getElementId(el))); - if (Array.isArray(selected)) { - return selected.filter(val => val === true).length > 0; - } - return selected; - }, aSec * 1000, `element (${new Locator(locator)}) still not enabled after ${aSec} sec`); - } + return this.browser.waitUntil(async () => { const res = await this._res(locator); if (!res || res.length === 0) { @@ -2077,12 +2052,7 @@ class WebDriver extends Helper { */ async waitForElement(locator, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this.$$(withStrictLocator(locator)); - return res && res.length; - }, aSec * 1000, `element (${(new Locator(locator))}) still not present on page after ${aSec} sec`); - } + return this.browser.waitUntil(async () => { const res = await this._res(locator); return res && res.length; @@ -2111,20 +2081,7 @@ class WebDriver extends Helper { const client = this.browser; const aSec = sec || this.options.waitForTimeoutInSeconds; let currUrl = ''; - if (isWebDriver5()) { - return client - .waitUntil(function () { - return this.getUrl().then((res) => { - currUrl = decodeUrl(res); - return currUrl.indexOf(urlPart) > -1; - }); - }, aSec * 1000).catch((e) => { - if (e.message.indexOf('timeout')) { - throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`); - } - throw e; - }); - } + return client .waitUntil(function () { return this.getUrl().then((res) => { @@ -2169,20 +2126,6 @@ class WebDriver extends Helper { async waitForText(text, sec = null, context = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; const _context = context || this.root; - if (isWebDriver5()) { - return this.browser.waitUntil( - async () => { - const res = await this.$$(withStrictLocator.call(this, _context)); - if (!res || res.length === 0) return false; - const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el))); - if (Array.isArray(selected)) { - return selected.filter(part => part.indexOf(text) >= 0).length > 0; - } - return selected.indexOf(text) >= 0; - }, aSec * 1000, - `element (${_context}) is not in DOM or there is no element(${_context}) with text "${text}" after ${aSec} sec`, - ); - } return this.browser.waitUntil( async () => { @@ -2206,20 +2149,7 @@ class WebDriver extends Helper { async waitForValue(field, value, sec = null) { const client = this.browser; const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return client.waitUntil( - async () => { - const res = await findFields.call(this, field); - if (!res || res.length === 0) return false; - const selected = await forEachAsync(res, async el => el.getValue()); - if (Array.isArray(selected)) { - return selected.filter(part => part.indexOf(value) >= 0).length > 0; - } - return selected.indexOf(value) >= 0; - }, aSec * 1000, - `element (${field}) is not in DOM or there is no element(${field}) with value "${value}" after ${aSec} sec`, - ); - } + return client.waitUntil( async () => { const res = await findFields.call(this, field); @@ -2242,17 +2172,7 @@ class WebDriver extends Helper { */ async waitForVisible(locator, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this.$$(withStrictLocator(locator)); - if (!res || res.length === 0) return false; - const selected = await forEachAsync(res, async el => el.isDisplayed()); - if (Array.isArray(selected)) { - return selected.filter(val => val === true).length > 0; - } - return selected; - }, aSec * 1000, `element (${new Locator(locator)}) still not visible after ${aSec} sec`); - } + return this.browser.waitUntil(async () => { const res = await this._res(locator); if (!res || res.length === 0) return false; @@ -2269,17 +2189,7 @@ class WebDriver extends Helper { */ async waitNumberOfVisibleElements(locator, num, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this.$$(withStrictLocator(locator)); - if (!res || res.length === 0) return false; - let selected = await forEachAsync(res, async el => el.isDisplayed()); - if (!Array.isArray(selected)) selected = [selected]; - selected = selected.filter(val => val === true); - return selected.length === num; - }, aSec * 1000, `The number of elements (${new Locator(locator)}) is not ${num} after ${aSec} sec`); - } return this.browser.waitUntil(async () => { const res = await this._res(locator); if (!res || res.length === 0) return false; @@ -2296,14 +2206,7 @@ class WebDriver extends Helper { */ async waitForInvisible(locator, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this.$$(withStrictLocator(locator)); - if (!res || res.length === 0) return true; - const selected = await forEachAsync(res, async el => el.isDisplayed()); - return !selected.length; - }, aSec * 1000, `element (${new Locator(locator)}) still visible after ${aSec} sec`); - } + return this.browser.waitUntil(async () => { const res = await this._res(locator); if (!res || res.length === 0) return true; @@ -2324,15 +2227,7 @@ class WebDriver extends Helper { */ async waitForDetached(locator, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => { - const res = await this._res(locator); - if (!res || res.length === 0) { - return true; - } - return false; - }, aSec * 1000, `element (${new Locator(locator)}) still on page after ${aSec} sec`); - } + return this.browser.waitUntil(async () => { const res = await this._res(locator); if (!res || res.length === 0) { @@ -2356,9 +2251,7 @@ class WebDriver extends Helper { } const aSec = sec || this.options.waitForTimeoutInSeconds; - if (isWebDriver5()) { - return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), aSec * 1000, ''); - } + return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), { timeout: aSec * 1000, timeoutMsg: '' }); } @@ -2388,18 +2281,6 @@ class WebDriver extends Helper { let target; const current = await this.browser.getWindowHandle(); - if (isWebDriver5()) { - await this.browser.waitUntil(async () => { - await this.browser.getWindowHandles().then((handles) => { - if (handles.indexOf(current) + num + 1 <= handles.length) { - target = handles[handles.indexOf(current) + num]; - } - }); - return target; - }, aSec * 1000, `There is no ability to switch to next tab with offset ${num}`); - return this.browser.switchToWindow(target); - } - await this.browser.waitUntil(async () => { await this.browser.getWindowHandles().then((handles) => { if (handles.indexOf(current) + num + 1 <= handles.length) { @@ -2419,18 +2300,6 @@ class WebDriver extends Helper { const current = await this.browser.getWindowHandle(); let target; - if (isWebDriver5()) { - await this.browser.waitUntil(async () => { - await this.browser.getWindowHandles().then((handles) => { - if (handles.indexOf(current) - num > -1) { - target = handles[handles.indexOf(current) - num]; - } - }); - return target; - }, aSec * 1000, `There is no ability to switch to previous tab with offset ${num}`); - return this.browser.switchToWindow(target); - } - await this.browser.waitUntil(async () => { await this.browser.getWindowHandles().then((handles) => { if (handles.indexOf(current) - num > -1) { @@ -3018,8 +2887,4 @@ function prepareLocateFn(context) { }; } -function isWebDriver5() { - return version && version.indexOf('5') === 0; -} - module.exports = WebDriver; From 36a7cd7f75f5b3dffd82b8a56667eabe170cf26a Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Sun, 12 Feb 2023 22:44:12 +0100 Subject: [PATCH 16/30] fix(playwright): requires param for handleDownloads (#3511) * fix(playwright): requires param for handleDownloads * update docs --- docs/helpers/FileSystem.md | 4 ++-- docs/helpers/Playwright.md | 2 +- lib/helper/FileSystem.js | 4 ++-- lib/helper/Playwright.js | 2 +- typings/tests/helpers/Playwright.types.ts | 2 +- typings/tests/helpers/PlaywrightTs.types.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/helpers/FileSystem.md b/docs/helpers/FileSystem.md index 4f9fe4df9..c7dfa0df1 100644 --- a/docs/helpers/FileSystem.md +++ b/docs/helpers/FileSystem.md @@ -124,10 +124,10 @@ Checks that file found by `seeFile` includes a text. ### waitForFile -Waits for file to be present in current directory. +Waits for the file to be present in the current directory. ```js -I.handleDownloads(); +I.handleDownloads('downloads/largeFilesName.txt'); I.click('Download large File'); I.amInPath('output/downloads'); I.waitForFile('largeFilesName.txt', 10); // wait 10 seconds for file diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index d368cdd58..c892c90f3 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -1139,7 +1139,7 @@ I.waitForFile('avatar.jpg', 5); #### Parameters -- `fileName` **[string][8]?** set filename for downloaded file +- `fileName` **[string][8]** set filename for downloaded file Returns **[Promise][14]<void>** diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js index 5db102038..b0cc5d090 100644 --- a/lib/helper/FileSystem.js +++ b/lib/helper/FileSystem.js @@ -71,10 +71,10 @@ class FileSystem extends Helper { } /** - * Waits for file to be present in current directory. + * Waits for the file to be present in the current directory. * * ```js - * I.handleDownloads(); + * I.handleDownloads('downloads/largeFilesName.txt'); * I.click('Download large File'); * I.amInPath('output/downloads'); * I.waitForFile('largeFilesName.txt', 10); // wait 10 seconds for file diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 6f9b1f514..9b0f28ca0 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1260,7 +1260,7 @@ class Playwright extends Helper { * * ``` * - * @param {string} [fileName] set filename for downloaded file + * @param {string} fileName set filename for downloaded file * @return {Promise} */ async handleDownloads(fileName) { diff --git a/typings/tests/helpers/Playwright.types.ts b/typings/tests/helpers/Playwright.types.ts index 67120aaee..5cfbb30c3 100644 --- a/typings/tests/helpers/Playwright.types.ts +++ b/typings/tests/helpers/Playwright.types.ts @@ -52,7 +52,7 @@ playwright.seeElement(str); // $ExpectType void playwright.dontSeeElement(str); // $ExpectType void playwright.seeElementInDOM(str); // $ExpectType void playwright.dontSeeElementInDOM(str); // $ExpectType void -playwright.handleDownloads(); // $ExpectType Promise +playwright.handleDownloads(str); // $ExpectType Promise playwright.click(str); // $ExpectType void playwright.click(str, str); // $ExpectType void playwright.click(str, null, { position }); // $ExpectType void diff --git a/typings/tests/helpers/PlaywrightTs.types.ts b/typings/tests/helpers/PlaywrightTs.types.ts index bec5bc941..22a512129 100644 --- a/typings/tests/helpers/PlaywrightTs.types.ts +++ b/typings/tests/helpers/PlaywrightTs.types.ts @@ -48,7 +48,7 @@ playwright.seeElement(str); // $ExpectType Promise playwright.dontSeeElement(str); // $ExpectType Promise playwright.seeElementInDOM(str); // $ExpectType Promise playwright.dontSeeElementInDOM(str); // $ExpectType Promise -playwright.handleDownloads(); // $ExpectType Promise +playwright.handleDownloads(str); // $ExpectType Promise playwright.click(str); // $ExpectType Promise playwright.click(str, str); // $ExpectType Promise playwright.click(str, null, { position }); // $ExpectType Promise From 429c60962e57808520abe147e5740ce92f030be3 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Sun, 12 Feb 2023 22:46:49 +0100 Subject: [PATCH 17/30] fix(gpo): codeceptjs config is contaminated when having includes() (#3531) * fix(gpo): codeceptjs config is contaminated when having includes() * fix: 3532 respect the current include section --- lib/command/generate.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/command/generate.js b/lib/command/generate.js index b1d604581..d735e1774 100644 --- a/lib/command/generate.js +++ b/lib/command/generate.js @@ -128,7 +128,9 @@ module.exports.pageObject = function (genPath, opts) { const name = lcfirst(result.name) + ucfirst(kind); let data = readConfig(configFile); config.include[name] = result.filename; - data = data.replace(/include[\s\S][^\}]*/i, `include: ${JSON.stringify(config.include).slice(0, -1)}`); + const currentInclude = `${data.match(/include:[\s\S][^\}]*/i)[0]}\n ${name}:${JSON.stringify(config.include[name])}`; + + data = data.replace(/include:[\s\S][^\}]*/i, `${currentInclude}`); fs.writeFileSync(configFile, beautify(data), 'utf-8'); From ac92fe539f3f6479c99aaede8d9dca2b639dea90 Mon Sep 17 00:00:00 2001 From: Daniel Rentz Date: Sun, 12 Feb 2023 22:47:24 +0100 Subject: [PATCH 18/30] JsDoc: Remove promise from `Actor.say` (#3535) The return type of `Actor.say` contains `Promise`. In TypeScript code, with the ESLint rule "@typescript-eslint/no-floating-promises" this causes lint errors for every `I.say` in the tests stating that a promise will not be awaited. Instead, add a comment similar to many other methods (`I.see`, `I.click`, ...) and leave the return type `void`. --- lib/actor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/actor.js b/lib/actor.js index 94b66ec2b..5b4332b68 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -16,8 +16,9 @@ class Actor { * add print comment method` * @param {string} msg * @param {string} color - * @return {Promise | undefined} * @inner + * + * âš ī¸ returns a promise which is synchronized internally by recorder */ say(msg, color = 'cyan') { return recorder.add(`say ${msg}`, () => { From 7bc422a8dbacf322e7e672f34b2261474d28b1f5 Mon Sep 17 00:00:00 2001 From: Aldo Velasco Date: Sun, 12 Feb 2023 15:47:43 -0600 Subject: [PATCH 19/30] fix(docs): typo in best practices (#3524) --- docs/best.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best.md b/docs/best.md index bdcde6542..dfb1a95f5 100644 --- a/docs/best.md +++ b/docs/best.md @@ -74,7 +74,7 @@ When a project is growing and more and more tests are required, it's time to thi Here is a recommended strategy what to store where: * Move site-wide actions into an **Actor** file (`custom_steps.js` file). Such actions like `login`, using site-wide common controls, like drop-downs, rich text editors, calendars. -* Move page-based actions and selectors into **Page Object**. All acitivities made on that page can go into methods of page object. If you test Single Page Application a PageObject should represent a screen of your application. +* Move page-based actions and selectors into **Page Object**. All activities made on that page can go into methods of page object. If you test Single Page Application a PageObject should represent a screen of your application. * When site-wide widgets are used, interactions with them should be placed in **Page Fragments**. This should be applied to global navigation, modals, widgets. * A custom action that requires some low-level driver access, should be placed into a **Helper**. For instance, database connections, complex mouse actions, email testing, filesystem, services access. From 2092cf7f9242228efdea754399c94153e09f2083 Mon Sep 17 00:00:00 2001 From: Jim Davis Date: Sun, 12 Feb 2023 16:48:41 -0500 Subject: [PATCH 20/30] bump version of @faker-js/faker used by plugin fakerTransform to latest version (#3515) Co-authored-by: Jim Davis From c7626b90a6bdefa099b407da1f6d7d663ec93f0e Mon Sep 17 00:00:00 2001 From: Michael Bodnarchuk Date: Sun, 12 Feb 2023 23:48:54 +0200 Subject: [PATCH 21/30] Retry befores (#3580) * retry for hooks initial * retry for before & beforeSuite * updated tests, refactored timeouts * fixed tests for BDD * fixed bdd test * a better hook for a tes * updated docs for timeouts and retries * fixed examples * simplified test --- docs/advanced.md | 70 +++++--- docs/basics.md | 155 +++++++++++++----- examples/checkout_test.js | 11 +- examples/codecept.config.js | 6 +- examples/user_helper.js | 14 +- lib/codecept.js | 1 + lib/interfaces/featureConfig.js | 1 + lib/listener/retry.js | 67 ++++++++ lib/listener/steps.js | 2 +- lib/listener/timeout.js | 57 +++++-- lib/scenario.js | 41 +++-- lib/utils.js | 6 + test/data/sandbox/codecept.workers.conf.js | 2 +- .../configs/retryHooks/codecept.conf.js | 12 ++ .../retryHooks/codecept.retry.obj.conf.js | 16 ++ .../codecept.retry.obj2.fail.conf.js | 19 +++ .../data/sandbox/configs/retryHooks/helper.js | 22 +++ .../configs/retryHooks/retry_async_test.js | 18 ++ .../configs/retryHooks/retry_before_spec.js | 9 + .../retryHooks/retry_before_suite_spec.js | 9 + .../retryHooks/retry_before_suite_test.js | 9 + .../configs/retryHooks/retry_helper_spec.js | 9 + .../configs/retryHooks/retry_helper_test.js | 9 + .../sandbox/configs/retryHooks/retry_test.js | 13 ++ .../timeouts/codecept.timeout.obj.conf.js | 16 ++ .../sandbox/configs/timeouts/suite_test.js | 4 +- .../configs/timeouts/suite_timeout_test.js | 2 +- test/runner/retry_hooks_test.js | 31 ++++ test/runner/run_workers_test.js | 2 + test/runner/timeout_test.js | 29 ++++ test/unit/bdd_test.js | 18 +- 31 files changed, 564 insertions(+), 116 deletions(-) create mode 100644 lib/listener/retry.js create mode 100644 test/data/sandbox/configs/retryHooks/codecept.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/codecept.retry.obj.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/codecept.retry.obj2.fail.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/helper.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_async_test.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_before_spec.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_before_suite_spec.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_before_suite_test.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_helper_spec.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_helper_test.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_test.js create mode 100644 test/data/sandbox/configs/timeouts/codecept.timeout.obj.conf.js create mode 100644 test/runner/retry_hooks_test.js diff --git a/docs/advanced.md b/docs/advanced.md index d1a9370ac..69df7345c 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -186,7 +186,7 @@ You can use this options for build your own [plugins](https://codecept.io/hooks/ }); ``` -## Timeout +## Timeout Tests can get stuck due to various reasons such as network connection issues, crashed browser, etc. This can make tests process hang. To prevent these situations timeouts can be used. Timeouts can be set explicitly for flaky parts of code, or implicitly in a config. @@ -236,38 +236,60 @@ A timeout for a group of tests can be set on Feature level via options. Feature('flaky tests', { timeout: 30 }) ``` -### Sum Up +### Timeout Confguration -Let's list all available timeout options. +Timeout rules can be set globally via config. -Timeouts can be set globally in config: +To set a timeout for all running tests provide a **number of seconds** to `timeout` config option: -```js -// in codecept.confg.js: -{ // ... - timeout: 30, // limit all tests in all suites to 30 secs - - plugins: { - stepTimeout: { - enabled: true, - timeout: 10, // limit all steps except waiters to 10 secs - } - } -} +```js +// inside codecept.conf.js or codecept.conf.ts +timeout: 30, // limit all tests in all suites to 30 secs ``` -or inside a test file: +It is possible to tune this configuration for a different groups of tests passing options as array and using `grep` option to filter tests: ```js -// limit all tests in this suite to 10 secs -Feature('tests with timeout', { timeout: 10 }); +// inside codecept.conf.js or codecept.conf.ts -// limit this test to 20 secs -Scenario('a test with timeout', { timeout: 20 }, ({ I }) => { - // limit step to 5 seconds - I.limitTime(5).click('Link'); -}); +timeout: [ + 10, // default timeout is 10secs + + // but increase timeout for slow tests + { + grep: '@slow', + Feature: 50 + }, +] +``` + +> â„šī¸ `grep` value can be string or regexp + +It is possible to set a timeout for Scenario or Feature: + +```js +// inside codecept.conf.js or codecept.conf.ts +timeout: [ + + // timeout for Feature with @slow in title + { + grep: '@slow', + Feature: 50 + }, + + // timeout for Scenario with 'flaky0' .. `flaky1` in title + { + // regexp can be passed to grep + grep: /flaky[0-9]/, + Scenario: 10 + }, + + // timeout for all suites + { + Feature: 20 + } +] ``` Global timeouts will be overridden by explicit timeouts of a test or steps. diff --git a/docs/basics.md b/docs/basics.md index fd17ab21a..a78ff8bda 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -345,7 +345,7 @@ If you face that error please make sure that all async functions are called with ## Running Tests -To launch tests use the `run` command, and to execute tests in [multiple browsers](/advanced/#multiple-browsers-execution) or [multiple threads](/advanced/#parallel-execution) use the `run-multiple` command. +To launch tests use the `run` command, and to execute tests in [multiple threads](/advanced/parallel) using `run-workers` command. ### Level of Detail @@ -394,7 +394,7 @@ It is recommended to [filter tests by tags](/advanced/#tags). ### Parallel Run -Since CodeceptJS 2.3, you can run tests in parallel by using NodeJS workers. This feature requires NodeJS >= 11.6. Use `run-workers` command with the number of workers (threads) to split tests. +Tests can be executed in parallel mode by using [NodeJS workers](https://nodejs.org/api/worker_threads.html). Use `run-workers` command with the number of workers (threads) to split tests into different workers. ``` npx codeceptjs run-workers 3 @@ -532,13 +532,55 @@ This can be configured in [screenshotOnFail Plugin](/plugins/#screenshotonfail) To see how the test was executed, use [stepByStepReport Plugin](/plugins/#stepbystepreport). It saves a screenshot of each passed step and shows them in a nice slideshow. +## Before + +Common preparation steps like opening a web page or logging in a user, can be placed in the `Before` or `Background` hooks: + +```js +Feature('CodeceptJS Demonstration'); + +Before(({ I }) => { // or Background + I.amOnPage('/documentation'); +}); + +Scenario('test some forms', ({ I }) => { + I.click('Create User'); + I.see('User is valid'); + I.dontSeeInCurrentUrl('/documentation'); +}); + +Scenario('test title', ({ I }) => { + I.seeInTitle('Example application'); +}); +``` + +Same as `Before` you can use `After` to run teardown for each scenario. + +## BeforeSuite + +If you need to run complex a setup before all tests and have to teardown this afterwards, you can use the `BeforeSuite` and `AfterSuite` functions. +`BeforeSuite` and `AfterSuite` have access to the `I` object, but `BeforeSuite/AfterSuite` don't have any access to the browser, because it's not running at this moment. +You can use them to execute handlers that will setup your environment. `BeforeSuite/AfterSuite` will work only for the file it was declared in (so you can declare different setups for files) + +```js +BeforeSuite(({ I }) => { + I.syncDown('testfolder'); +}); + +AfterSuite(({ I }) => { + I.syncUp('testfolder'); + I.clearDir('testfolder'); +}); +``` + ## Retries ### Auto Retry -You can auto-retry a failed step by enabling [retryFailedStep Plugin](/plugins/#retryfailedstep). +Each failed step is auto-retried by default via [retryFailedStep Plugin](/plugins/#retryfailedstep). +If this is not expected, this plugin can be disabled in a config. -> **[retryFailedStep plugin](/plugins/#retryfailedstep) is enabled by default** for new setups +> **[retryFailedStep plugin](/plugins/#retryfailedstep) is enabled by default** incide global configuration ### Retry Step @@ -608,69 +650,102 @@ Scenario('Really complex', { retries: 2 },({ I }) => {}); This scenario will be restarted two times on a failure. Unlike retry step, there is no `when` condition supported for retries on a scenario level. +### Retry Before + +To retry `Before`, `BeforeSuite`, `After`, `AfterSuite` hooks, add corresponding option to a `Feature`: + +* `retryBefore` +* `retryBeforeSuite` +* `retryAfter` +* `retryAfterSuite` + +For instance, to retry Before hook 3 times: + +```js +Feature('this have a flaky Befure', { retryBefore: 3 }) +``` + +Multiple options of different values can be set at the same time + ### Retry Feature To set this option for all scenarios in a file, add `retry` to a feature: ```js Feature('Complex JS Stuff').retry(3); +// or +Feature('Complex JS Stuff', { retries: 3 }) ``` Every Scenario inside this feature will be rerun 3 times. You can make an exception for a specific scenario by passing the `retries` option to a Scenario. -### Retry Run - -On the highest level of the "retry pyramid" there is an option to retry a complete run multiple times. -Even this is the slowest option of all, it can be helpful to detect flaky tests. +### Retry Configuration -[`run-rerun`](https://codecept.io/commands/#run-rerun) command will restart the run multiple times to values you provide. You can set minimal and maximal number of restarts in configuration file. +It is possible to set retry rules globally via `retry` config option. The configuration is flexible and allows multiple formats. +The simplest config would be: +```js +// inside codecept.conf.js +retry: 3 ``` -npx codeceptjs run-rerun -``` - -## Before +This will enable Feature Retry for all executed feature, retrying failing tests 3 times. -Common preparation steps like opening a web page or logging in a user, can be placed in the `Before` or `Background` hooks: +An object can be used to tune retries of a Before/After hook, Scenario or Feature ```js -Feature('CodeceptJS Demonstration'); +// inside codecept.conf.js +retry: { + Feature: ..., + Scenario: ..., + Before: ..., + BeforeSuite: ..., + After: ..., + AfterSuite: ..., +} +``` -Before(({ I }) => { // or Background - I.amOnPage('/documentation'); -}); +Multiple retry configs can be added via array. To use different retry configs for different subset of tests use `grep` option inside a retry config: + +```js +// inside codecept.conf.js +retry: [ + { + // enable this config only for flaky tests + grep: '@flaky', + Before: 3 + Scenario: 3 + }, + { + // retry less when running slow tests + grep: '@slow' + Scenario: 1 + Before: 1 + }, { + // retry all BeforeSuite + BeforeSuite: 3 + } +] +``` -Scenario('test some forms', ({ I }) => { - I.click('Create User'); - I.see('User is valid'); - I.dontSeeInCurrentUrl('/documentation'); -}); +When using `grep` with `Before`, `After`, `BeforeSuite`, `AfterSuite`, a suite title will be checked for included value. -Scenario('test title', ({ I }) => { - I.seeInTitle('Example application'); -}); -``` +> â„šī¸ `grep` value can be string or regexp -Same as `Before` you can use `After` to run teardown for each scenario. +Rules are applied in the order of array element, so the last option will override a previous one. Global retries config can be overridden in a file as described previously. -## BeforeSuite +### Retry Run -If you need to run complex a setup before all tests and have to teardown this afterwards, you can use the `BeforeSuite` and `AfterSuite` functions. -`BeforeSuite` and `AfterSuite` have access to the `I` object, but `BeforeSuite/AfterSuite` don't have any access to the browser, because it's not running at this moment. -You can use them to execute handlers that will setup your environment. `BeforeSuite/AfterSuite` will work only for the file it was declared in (so you can declare different setups for files) +On the highest level of the "retry pyramid" there is an option to retry a complete run multiple times. +Even this is the slowest option of all, it can be helpful to detect flaky tests. -```js -BeforeSuite(({ I }) => { - I.syncDown('testfolder'); -}); +[`run-rerun`](https://codecept.io/commands/#run-rerun) command will restart the run multiple times to values you provide. You can set minimal and maximal number of restarts in configuration file. -AfterSuite(({ I }) => { - I.syncUp('testfolder'); - I.clearDir('testfolder'); -}); ``` +npx codeceptjs run-rerun +``` + [Here are some ideas](https://github.com/codeceptjs/CodeceptJS/pull/231#issuecomment-249554933) on where to use BeforeSuite hooks. diff --git a/examples/checkout_test.js b/examples/checkout_test.js index 85dd95b5e..4f676eedd 100644 --- a/examples/checkout_test.js +++ b/examples/checkout_test.js @@ -1,14 +1,17 @@ -Feature('Checkout'); +Feature('Checkout', { retryBefore: 3 }); + +const i = 0; Before(({ I }) => { I.amOnPage('https://getbootstrap.com/docs/4.0/examples/checkout/'); + I.failNTimes(3); }); Scenario('It should fill in checkout page', async ({ I }) => { I.fillField('#lastName', 'mik'); - await eachElement('tick all checkboxes', '.custom-checkbox label', async (el) => { - await el.click(); - }); + // await eachElement('tick all checkboxes', '.custom-checkbox label', async (el) => { + // await el.click(); + // }); await retryTo((retryNum) => { I.fillField('Promo code', '123345'); I.click('Redeem'); diff --git a/examples/codecept.config.js b/examples/codecept.config.js index bb2331e3f..9d171ee50 100644 --- a/examples/codecept.config.js +++ b/examples/codecept.config.js @@ -1,5 +1,6 @@ exports.config = { output: './output', + retry: 3, helpers: { Playwright: { url: 'http://localhost', @@ -14,6 +15,9 @@ exports.config = { show: !process.env.HEADLESS, }, REST: {}, + User: { + require: './user_helper.js', + }, }, include: { I: './custom_steps.js', @@ -60,7 +64,7 @@ exports.config = { }, }, tests: './*_test.js', - timeout: 100, + // timeout: 100, multiple: { parallel: { chunks: 2, diff --git a/examples/user_helper.js b/examples/user_helper.js index 03edf095f..aaf4879e8 100644 --- a/examples/user_helper.js +++ b/examples/user_helper.js @@ -1,14 +1,18 @@ const assert = require('assert'); -const Helper = require('../lib/helper'); class User extends Helper { - // before/after hooks + _beforeSuite() { + } + _before() { - // remove if not used + this.i = 0; } - _after() { - // remove if not used + failNTimes(n) { + this.i++; + // this.i++; + console.log(this.i, n); + if (this.i < n) throw new Error('ups, error'); } // add custom methods here diff --git a/lib/codecept.js b/lib/codecept.js index c922812dd..b79c814cd 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -98,6 +98,7 @@ class Codecept { runHook(require('./listener/artifacts')); runHook(require('./listener/config')); runHook(require('./listener/helpers')); + runHook(require('./listener/retry')); runHook(require('./listener/timeout')); runHook(require('./listener/exit')); diff --git a/lib/interfaces/featureConfig.js b/lib/interfaces/featureConfig.js index 3c014c6ce..ef301f7f7 100644 --- a/lib/interfaces/featureConfig.js +++ b/lib/interfaces/featureConfig.js @@ -19,6 +19,7 @@ class FeatureConfig { * Set timeout for this suite * @param {number} timeout * @returns {this} + * @deprecated */ timeout(timeout) { console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`); diff --git a/lib/listener/retry.js b/lib/listener/retry.js new file mode 100644 index 000000000..9b5fcf989 --- /dev/null +++ b/lib/listener/retry.js @@ -0,0 +1,67 @@ +const event = require('../event'); +const output = require('../output'); +const Config = require('../config'); +const { isNotSet } = require('../utils'); + +const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']; + +module.exports = function () { + event.dispatcher.on(event.suite.before, (suite) => { + let retryConfig = Config.get('retry'); + if (!retryConfig) return; + + if (Number.isInteger(+retryConfig)) { + // is number + const retryNum = +retryConfig; + output.log(`Retries: ${retryNum}`); + suite.retries(retryNum); + return; + } + + if (!Array.isArray(retryConfig)) { + retryConfig = [retryConfig]; + } + + for (const config of retryConfig) { + if (config.grep) { + if (!suite.title.includes(config.grep)) continue; + } + + hooks.filter(hook => !!config[hook]).forEach((hook) => { + if (isNotSet(suite.opts[`retry${hook}`])) suite.opts[`retry${hook}`] = config[hook]; + }); + + if (config.Feature) { + if (isNotSet(suite.retries())) suite.retries(config.Feature); + } + + output.log(`Retries: ${JSON.stringify(config)}`); + } + }); + + event.dispatcher.on(event.test.before, (test) => { + let retryConfig = Config.get('retry'); + if (!retryConfig) return; + + if (Number.isInteger(+retryConfig)) { + return; + } + + if (!Array.isArray(retryConfig)) { + retryConfig = [retryConfig]; + } + + retryConfig = retryConfig.filter(config => !!config.Scenario); + + for (const config of retryConfig) { + if (config.grep) { + if (!test.title.includes(config.grep)) continue; + } + + if (config.Scenario) { + if (isNotSet(test.retries())) test.retries(config.Scenario); + output.log(`Retries: ${config.Scenario}`); + } + } + }); +}; diff --git a/lib/listener/steps.js b/lib/listener/steps.js index 9a99303ae..53e252665 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -81,6 +81,6 @@ module.exports = function () { if (store.debugMode) return; step.finishedAt = +new Date(); if (step.startedAt) step.duration = step.finishedAt - step.startedAt; - debug(`Step '${step}' finished; Duration: ${step.duration}ms`); + debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`); }); }; diff --git a/lib/listener/timeout.js b/lib/listener/timeout.js index 90a9b1d8b..3b3a3d1c4 100644 --- a/lib/listener/timeout.js +++ b/lib/listener/timeout.js @@ -7,7 +7,7 @@ const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER; module.exports = function () { let timeout; - let timeoutStack = []; + let suiteTimeout = []; let currentTest; let currentTimeout; @@ -17,21 +17,52 @@ module.exports = function () { } event.dispatcher.on(event.suite.before, (suite) => { - timeoutStack = []; - const globalTimeout = Config.get('timeout'); - if (globalTimeout) { - if (globalTimeout >= 1000) { - console.log(`Warning: Timeout was set to ${globalTimeout}secs.\nGlobal timeout should be specified in seconds.`); + suiteTimeout = []; + let timeoutConfig = Config.get('timeout'); + + if (timeoutConfig) { + if (!Number.isNaN(+timeoutConfig)) { + checkForSeconds(timeoutConfig); + suiteTimeout.push(timeoutConfig); + } + + if (!Array.isArray(timeoutConfig)) { + timeoutConfig = [timeoutConfig]; + } + + for (const config of timeoutConfig.filter(c => !!c.Feature)) { + if (config.grep) { + if (!suite.title.includes(config.grep)) continue; + } + suiteTimeout.push(config.Feature); } - timeoutStack.push(globalTimeout); } - if (suite.totalTimeout) timeoutStack.push(suite.totalTimeout); - output.log(`Timeouts: ${timeoutStack}`); + + if (suite.totalTimeout) suiteTimeout.push(suite.totalTimeout); + output.log(`Timeouts: ${suiteTimeout}`); }); event.dispatcher.on(event.test.before, (test) => { currentTest = test; - timeout = test.totalTimeout || timeoutStack[timeoutStack.length - 1]; + let testTimeout = null; + + let timeoutConfig = Config.get('timeout'); + + if (typeof timeoutConfig === 'object' || Array.isArray(timeoutConfig)) { + if (!Array.isArray(timeoutConfig)) { + timeoutConfig = [timeoutConfig]; + } + + for (const config of timeoutConfig.filter(c => !!c.Scenario)) { + console.log('Test Timeout', config, test.title.includes(config.grep)); + if (config.grep) { + if (!test.title.includes(config.grep)) continue; + } + testTimeout = config.Scenario; + } + } + + timeout = test.totalTimeout || testTimeout || suiteTimeout[suiteTimeout.length - 1]; if (!timeout) return; currentTimeout = timeout; output.debug(`Test Timeout: ${timeout}s`); @@ -70,3 +101,9 @@ module.exports = function () { } }); }; + +function checkForSeconds(timeout) { + if (timeout >= 1000) { + console.log(`Warning: Timeout was set to ${timeout}secs.\nGlobal timeout should be specified in seconds.`); + } +} diff --git a/lib/scenario.js b/lib/scenario.js index 4e4090d03..416d6e7d0 100644 --- a/lib/scenario.js +++ b/lib/scenario.js @@ -1,7 +1,8 @@ +const promiseRetry = require('promise-retry'); const event = require('./event'); const recorder = require('./recorder'); const assertThrown = require('./assert/throws'); -const { isAsyncFunction } = require('./utils'); +const { ucfirst, isAsyncFunction } = require('./utils'); const parser = require('./parser'); const injectHook = function (inject, suite) { @@ -124,17 +125,31 @@ module.exports.injected = function (fn, suite, hookName) { if (!fn) throw new Error('fn is not defined'); event.emit(event.hook.started, suite); + + this.test.body = fn.toString(); + if (!recorder.isRunning()) { - recorder.start(); recorder.errHandler((err) => { errHandler(err); }); } - this.test.body = fn.toString(); + const opts = suite.opts || {}; + const retries = opts[`retry${ucfirst(hookName)}`] || 0; - if (isAsyncFunction(fn)) { - fn.call(this, getInjectedArguments(fn)).then(() => { + promiseRetry(async (retry) => { + try { + recorder.startUnlessRunning(); + await fn.call(this, getInjectedArguments(fn)); + await recorder.promise(); + } catch (err) { + retry(err); + } finally { + recorder.stop(); + recorder.start(); + } + }, { retries }) + .then(() => { recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite)); recorder.add(`finish ${hookName} hook`, () => done()); recorder.catch(); @@ -146,22 +161,6 @@ module.exports.injected = function (fn, suite, hookName) { }); recorder.add('fire hook.failed', () => event.emit(event.hook.failed, suite, e)); }); - return; - } - - try { - fn.call(this, getInjectedArguments(fn)); - recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite)); - recorder.add(`finish ${hookName} hook`, () => done()); - recorder.catch(); - } catch (err) { - recorder.throw(err); - recorder.catch((e) => { - const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr(); - errHandler(err); - }); - recorder.add('fire hook.failed', () => event.emit(event.hook.failed, suite, err)); - } }; }; diff --git a/lib/utils.js b/lib/utils.js index 52b1d088e..a4b8a2655 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -449,3 +449,9 @@ module.exports.requireWithFallback = function (...packages) { throw new Error(`Cannot find modules ${packages.join(',')}`); }; + +module.exports.isNotSet = function (obj) { + if (obj === null) return true; + if (obj === undefined) return true; + return false; +}; diff --git a/test/data/sandbox/codecept.workers.conf.js b/test/data/sandbox/codecept.workers.conf.js index d5bebf88a..0439ccdaa 100644 --- a/test/data/sandbox/codecept.workers.conf.js +++ b/test/data/sandbox/codecept.workers.conf.js @@ -15,7 +15,7 @@ exports.config = { setTimeout(() => { process.stdout.write('b2'); done(); - }, 1000); + }, 100); }); }, mocha: {}, diff --git a/test/data/sandbox/configs/retryHooks/codecept.conf.js b/test/data/sandbox/configs/retryHooks/codecept.conf.js new file mode 100644 index 000000000..1301cf055 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.conf.js @@ -0,0 +1,12 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + CustomHelper: { + require: './helper.js', + }, + }, + bootstrap: null, + mocha: {}, + name: 'retryHooks', +}; diff --git a/test/data/sandbox/configs/retryHooks/codecept.retry.obj.conf.js b/test/data/sandbox/configs/retryHooks/codecept.retry.obj.conf.js new file mode 100644 index 000000000..fe50a2f69 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.retry.obj.conf.js @@ -0,0 +1,16 @@ +exports.config = { + tests: './*_spec.js', + output: './output', + helpers: { + CustomHelper: { + require: './helper.js', + }, + }, + retry: { + BeforeSuite: 3, + Before: 3, + }, + bootstrap: null, + mocha: {}, + name: 'retryHooks', +}; diff --git a/test/data/sandbox/configs/retryHooks/codecept.retry.obj2.fail.conf.js b/test/data/sandbox/configs/retryHooks/codecept.retry.obj2.fail.conf.js new file mode 100644 index 000000000..ce9b17f07 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.retry.obj2.fail.conf.js @@ -0,0 +1,19 @@ +exports.config = { + tests: './*_spec.js', + output: './output', + helpers: { + CustomHelper: { + require: './helper.js', + }, + }, + retry: [ + { + grep: 'no timeout', + BeforeSuite: 3, + Before: 3, + }, + ], + bootstrap: null, + mocha: {}, + name: 'retryHooks', +}; diff --git a/test/data/sandbox/configs/retryHooks/helper.js b/test/data/sandbox/configs/retryHooks/helper.js new file mode 100644 index 000000000..a80715ba3 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/helper.js @@ -0,0 +1,22 @@ +class CustomHelper extends Helper { + _beforeSuite() { + this.i = 0; + } + + _before() { + this.i = 0; + } + + async failIfNotWorks() { + return new Promise((resolve, reject) => { + this.i++; + console.log('check if i <3', this.i); + setTimeout(() => { + if (this.i >= 3) resolve(); + reject(new Error('not works')); + }, 0); + }); + } +} + +module.exports = CustomHelper; diff --git a/test/data/sandbox/configs/retryHooks/retry_async_test.js b/test/data/sandbox/configs/retryHooks/retry_async_test.js new file mode 100644 index 000000000..76c663591 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_async_test.js @@ -0,0 +1,18 @@ +Feature('Retry #Async hooks', { retryBefore: 2 }); + +let i = 0; + +Before(async ({ I }) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + console.log('ok', i, new Date()); + i++; + if (i < 3) reject(new Error('not works')); + resolve(); + }, 0); + }); +}); + +Scenario('async hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_before_spec.js b/test/data/sandbox/configs/retryHooks/retry_before_spec.js new file mode 100644 index 000000000..08c37b548 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_before_spec.js @@ -0,0 +1,9 @@ +Feature('Fail #Before hook'); + +Before(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_before_suite_spec.js b/test/data/sandbox/configs/retryHooks/retry_before_suite_spec.js new file mode 100644 index 000000000..359649a2f --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_before_suite_spec.js @@ -0,0 +1,9 @@ +Feature('Retry #BeforeSuite helper hooks'); + +BeforeSuite(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_before_suite_test.js b/test/data/sandbox/configs/retryHooks/retry_before_suite_test.js new file mode 100644 index 000000000..3430af186 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_before_suite_test.js @@ -0,0 +1,9 @@ +Feature('Retry #BeforeSuite helper hooks', { retryBeforeSuite: 3 }).retry(3); + +BeforeSuite(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_helper_spec.js b/test/data/sandbox/configs/retryHooks/retry_helper_spec.js new file mode 100644 index 000000000..b5a6e5da8 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_helper_spec.js @@ -0,0 +1,9 @@ +Feature('Retry #Helper hooks'); + +Before(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_helper_test.js b/test/data/sandbox/configs/retryHooks/retry_helper_test.js new file mode 100644 index 000000000..adb83a2ab --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_helper_test.js @@ -0,0 +1,9 @@ +Feature('Retry #Helper hooks', { retryBefore: 2 }); + +Before(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_test.js b/test/data/sandbox/configs/retryHooks/retry_test.js new file mode 100644 index 000000000..2c2aa6883 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_test.js @@ -0,0 +1,13 @@ +Feature('Retry #Before hooks', { retryBefore: 2 }); + +let i = 0; + +Before(({ I }) => { + console.log('ok', i, new Date()); + i++; + if (i < 3) throw new Error('not works'); +}); + +Scenario('works', () => { + console.log('works'); +}); diff --git a/test/data/sandbox/configs/timeouts/codecept.timeout.obj.conf.js b/test/data/sandbox/configs/timeouts/codecept.timeout.obj.conf.js new file mode 100644 index 000000000..1334fbf12 --- /dev/null +++ b/test/data/sandbox/configs/timeouts/codecept.timeout.obj.conf.js @@ -0,0 +1,16 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + CustomHelper: { + require: './customHelper.js', + }, + }, + timeout: [ + { + grep: 'no timeout', + Scenario: 0.3, + }, + ], + name: 'steps', +}; diff --git a/test/data/sandbox/configs/timeouts/suite_test.js b/test/data/sandbox/configs/timeouts/suite_test.js index 9e723bf77..64a2e015e 100644 --- a/test/data/sandbox/configs/timeouts/suite_test.js +++ b/test/data/sandbox/configs/timeouts/suite_test.js @@ -1,10 +1,10 @@ Feature('no timeout'); -Scenario('no timeout test', ({ I }) => { +Scenario('no timeout test #first', ({ I }) => { I.waitForSleep(1000); }); -Scenario('timeout test in 0.5', { timeout: 0.5 }, ({ I }) => { +Scenario('timeout test in 0.5 #second', { timeout: 0.5 }, ({ I }) => { I.waitForSleep(1000); }); diff --git a/test/data/sandbox/configs/timeouts/suite_timeout_test.js b/test/data/sandbox/configs/timeouts/suite_timeout_test.js index 29166d0bf..4924411e8 100644 --- a/test/data/sandbox/configs/timeouts/suite_timeout_test.js +++ b/test/data/sandbox/configs/timeouts/suite_timeout_test.js @@ -4,6 +4,6 @@ Scenario('no timeout', ({ I }) => { I.waitForSleep(3000); }); -Scenario('timeout in 1', { timeout: 1 }, ({ I }) => { +Scenario('timeout in 1 #fourth', { timeout: 1 }, ({ I }) => { I.waitForSleep(3000); }); diff --git a/test/runner/retry_hooks_test.js b/test/runner/retry_hooks_test.js new file mode 100644 index 000000000..f9c135c85 --- /dev/null +++ b/test/runner/retry_hooks_test.js @@ -0,0 +1,31 @@ +const expect = require('expect'); +const exec = require('child_process').exec; +const { codecept_dir, codecept_run } = require('./consts'); + +const debug_this_test = false; + +const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose || debug_this_test ? '--verbose' : ''} --config ${codecept_dir}/configs/retryHooks/${config} ${grep ? `--grep "${grep}"` : ''}`; + +describe('CodeceptJS Retry Hooks', function () { + this.timeout(10000); + + ['#Async ', '#Before ', '#BeforeSuite ', '#Helper '].forEach(retryHook => { + it(`run ${retryHook} config`, (done) => { + exec(config_run_config('codecept.conf.js', retryHook), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).toContain('OK | 1 passed'); + done(); + }); + }); + }); + + ['#Before ', '#BeforeSuite '].forEach(retryHook => { + it(`should ${retryHook} set hook retries from global config`, (done) => { + exec(config_run_config('codecept.retry.obj.conf.js', retryHook), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).toContain('OK | 1 passed'); + done(); + }); + }); + }); +}); diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js index 9a2c8b4e9..0ae29d853 100644 --- a/test/runner/run_workers_test.js +++ b/test/runner/run_workers_test.js @@ -17,7 +17,9 @@ describe('CodeceptJS Workers Runner', function () { it('should run tests in 3 workers', function (done) { 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/timeout_test.js b/test/runner/timeout_test.js index f6aa46854..64e53c2b9 100644 --- a/test/runner/timeout_test.js +++ b/test/runner/timeout_test.js @@ -65,4 +65,33 @@ describe('CodeceptJS Timeouts', function () { done(); }); }); + + it('should override timeout config from global object', (done) => { + exec(config_run_config('codecept.timeout.obj.conf.js', '#first', true), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).toContain('Timeout 0.3s exceeded'); + expect(err).toBeTruthy(); + done(); + }); + }); + + it('should override timeout config from global object but respect local value', (done) => { + exec(config_run_config('codecept.timeout.obj.conf.js', '#second'), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).not.toContain('Timeout 0.3s exceeded'); + expect(stdout).toContain('Timeout 0.5s exceeded'); + expect(err).toBeTruthy(); + done(); + }); + }); + + it('should respect grep when overriding config from global config', (done) => { + exec(config_run_config('codecept.timeout.obj.conf.js', '#fourth'), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).not.toContain('Timeout 0.3s exceeded'); + expect(stdout).toContain('Timeout 1s exceeded'); + expect(err).toBeTruthy(); + done(); + }); + }); }); diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index 17e3739bb..baa8f4dbb 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -273,7 +273,7 @@ describe('BDD', () => { expect(500.30).is.equal(fn(fn.params)); }); - it('should attach before hook for Background', () => { + it('should attach before hook for Background', (finish) => { const text = ` Feature: checkout process @@ -284,13 +284,19 @@ describe('BDD', () => { Then I am shopping `; let sum = 0; - Given('I am logged in as customer', () => sum++); - Then('I am shopping', () => sum++); + function incrementSum() { + sum++; + } + Given('I am logged in as customer', incrementSum); + Then('I am shopping', incrementSum); const suite = run(text); - const done = () => { }; + const done = () => {}; + suite._beforeEach.forEach(hook => hook.run(done)); - suite.tests[0].fn(done); - expect(2).is.equal(sum); + suite.tests[0].fn(() => { + expect(sum).is.equal(2); + finish(); + }); }); it('should execute scenario outlines', (done) => { From 02af78badc2a0a41757f05f35a34a4a72e7e3f0f Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Wed, 15 Feb 2023 03:10:48 +0100 Subject: [PATCH 22/30] fix: inform user when there is no delete request is configured (#3525) --- lib/helper/ApiDataFactory.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index 24014fbb5..7acadd6ad 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -230,7 +230,7 @@ class ApiDataFactory extends Helper { } _after() { - if (!this.config.cleanup) { + if (!this.config.cleanup || this.config.cleanup === false) { return Promise.resolve(); } const promises = []; @@ -244,7 +244,6 @@ class ApiDataFactory extends Helper { promises.push(this._requestDelete(factoryName, createdItems[id])); } } - return Promise.all(promises); } @@ -379,7 +378,9 @@ Current file error: ${err.message}`); if (!request) { const method = Object.keys(this.factories[factory].delete)[0]; + const url = this.factories[factory].delete[method].replace('{id}', id); + request = { method, url, @@ -388,6 +389,10 @@ Current file error: ${err.message}`); request.baseURL = this.config.endpoint; + if (request.url.match(/^undefined/)) { + return this.debugSection('Please configure the delete request in your ApiDataFactory helper', 'delete: () => ({ method: \'DELETE\', url: \'/api/users\' })'); + } + return this.restHelper._executeRequest(request).then(() => { const idx = this.created[factory].indexOf(id); this.debugSection('Deleted Id', `Id: ${id}`); From da049146c32086c649b2dc99ef9e76a9a8ce18e8 Mon Sep 17 00:00:00 2001 From: Michael Bodnarchuk Date: Wed, 15 Feb 2023 04:35:10 +0200 Subject: [PATCH 23/30] 3.4.0 release (#3584) * preparing release * updated deps * updated deps * fixed unit tests * fixed typings * fixed log * fixed bdd test * fixed run-rerun error * updated helpers to use package * fixed tests * fixed api tests * fixed tests, added tutorial --- CHANGELOG.md | 47 ++ docs/advanced.md | 4 +- docs/plugins.md | 275 ++---------- docs/reports.md | 56 --- docs/tutorial.md | 271 ++++++++++++ docs/typescript.md | 8 +- examples/codecept.config.js | 1 - lib/cli.js | 6 +- lib/codecept.js | 2 +- lib/command/gherkin/snippets.js | 12 +- lib/command/init.js | 8 - lib/command/run-workers.js | 9 +- lib/command/utils.js | 10 - lib/command/workers/runTests.js | 4 +- lib/helper.js | 2 - lib/helper/ApiDataFactory.js | 2 +- lib/helper/Appium.js | 23 +- lib/helper/FileSystem.js | 2 +- lib/helper/GraphQL.js | 2 +- lib/helper/GraphQLDataFactory.js | 2 +- lib/helper/JSONResponse.js | 2 +- lib/helper/Mochawesome.js | 2 +- lib/helper/Nightmare.js | 2 +- lib/helper/Playwright.js | 2 +- lib/helper/Protractor.js | 2 +- lib/helper/Puppeteer.js | 2 +- lib/helper/REST.js | 2 +- lib/helper/TestCafe.js | 2 +- lib/helper/WebDriver.js | 2 +- lib/interfaces/bdd.js | 2 +- lib/interfaces/gherkin.js | 63 +-- lib/listener/exit.js | 4 +- lib/listener/retry.js | 2 +- lib/mochaFactory.js | 6 +- lib/plugin/allure.js | 418 +----------------- lib/workers.js | 11 +- package.json | 25 +- test/data/sandbox/codecept.customworker.js | 12 +- .../features/step_definitions/my_steps.js | 2 +- test/data/sandbox/workers_helper.js | 2 +- test/graphql/GraphQLDataFactory_test.js | 1 + test/graphql/GraphQL_test.js | 1 + test/helper/AppiumWeb_test.js | 1 + test/helper/Appium_test.js | 1 + test/helper/JSONResponse_test.js | 1 + test/helper/Nightmare_test.js | 1 + test/helper/Playwright_test.js | 1 + test/helper/Puppeteer_test.js | 1 + test/helper/TestCafe_test.js | 2 + test/helper/WebDriver_test.js | 1 + test/rest/ApiDataFactory_test.js | 1 + test/rest/REST_test.js | 1 + test/runner/allure_test.js | 147 ------ test/runner/bdd_test.js | 1 + test/runner/init_test.js | 3 +- test/unit/bdd_test.js | 16 +- test/unit/worker_test.js | 21 +- typings/index.d.ts | 77 +++- 58 files changed, 586 insertions(+), 1003 deletions(-) create mode 100644 docs/tutorial.md delete mode 100644 test/runner/allure_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 30151ecf4..35ff3721a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +## 3.4.0 + +* **Updated to latest mocha and modern Cucumber** +* **Allure plugin moved to [@codeceptjs/allure-legacy](https://github.com/codeceptjs/allure-legacy) package**. This happened because allure-commons package v1 was not updated and caused vulnarabilities. Fixes #3422. We don't plan to maintain allure v2 plugin so it's up to community to take this initiative. Current allure plugin will print a warning message without interfering the run, so it won't accidentally fail your builds. +* Added ability to **[retry Before](https://codecept.io/basics/#retry-before), BeforeSuite, After, AfterSuite** hooks by @davertmik: +```js +Feature('flaky Before & BeforeSuite', { retryBefore: 2, retryBeforeSuite: 3 }) +``` + +* **Flexible [retries configuration](https://codecept.io/basics/#retry-configuration) introduced** by @davertmik: + +```js +retry: [ + { + // enable this config only for flaky tests + grep: '@flaky', + Before: 3 // retry Before 3 times + Scenario: 3 // retry Scenario 3 times + }, + { + // retry less when running slow tests + grep: '@slow' + Scenario: 1 + Before: 1 + }, { + // retry all BeforeSuite 3 times + BeforeSuite: 3 + } +] +``` +* **Flexible [timeout configuration](https://codecept.io/advanced/#timeout-configuration)** introduced by @davertmik: + +```js +timeout: [ + 10, // default timeout is 10secs + { // but increase timeout for slow tests + grep: '@slow', + Feature: 50 + }, +] +``` + +* JsDoc: Removed promise from `I.say`. See #3535 by @danielrentz +* [Playwright] `handleDownloads` requires now a filename param. See #3511 by @PeterNgTr +* [WebDriver] Added support for v8, removed support for webdriverio v5 and lower. See #3578 by @PeterNgTr + + ## 3.3.7 đŸ›Šī¸ Features diff --git a/docs/advanced.md b/docs/advanced.md index 69df7345c..b5b686918 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -236,7 +236,9 @@ A timeout for a group of tests can be set on Feature level via options. Feature('flaky tests', { timeout: 30 }) ``` -### Timeout Confguration +### Timeout Confguration + + Timeout rules can be set globally via config. diff --git a/docs/plugins.md b/docs/plugins.md index 1d363e584..76a0e4182 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -7,167 +7,6 @@ title: Plugins -## addAttachment - -Add an attachment to the current test case - -### Parameters - -- `name` **[string][1]** Name of the attachment -- `buffer` **[Buffer][2]** Buffer of the attachment -- `type` **[string][1]** MIME type of the attachment - -## addLabel - -Adds a label with the given name and value to the current test in the Allure report - -### Parameters - -- `name` **[string][1]** name of the label to add -- `value` **[string][1]** value of the label to add - -## addParameter - -Adds a parameter with the given kind, name, and value to the current test in the Allure report - -### Parameters - -- `kind` **[string][1]** kind of the parameter to add -- `name` **[string][1]** name of the parameter to add -- `value` **[string][1]** value of the parameter to add - -## addScreenDiff - -Add a special screen diff block to the current test case - -### Parameters - -- `name` **[string][1]** Name of the screen diff block -- `expectedImg` **[string][1]** string representing the contents of the expected image file encoded in base64 -- `actualImg` **[string][1]** string representing the contents of the actual image file encoded in base64 -- `diffImg` **[string][1]** string representing the contents of the diff image file encoded in base64. - Could be generated by image comparison lib like "pixelmatch" or alternative - -## createStep - -A method for creating a step in a test case. - -### Parameters - -- `name` **[string][1]** The name of the step. -- `stepFunc` **[Function][3]** The function that should be executed for this step. (optional, default `()=>{}`) - -Returns **any** The result of the step function. - -## setDescription - -Set description for the current test case - -### Parameters - -- `description` **[string][1]** Description for the test case -- `type` **[string][1]** MIME type of the description (optional, default `'text/plain'`) - -## allure - -Allure reporter - -![][4] - -Enables Allure reporter. - -#### Usage - -To start please install `allure-commandline` package (which requires Java 8) - - npm install -g allure-commandline --save-dev - -Add this plugin to config file: - -```js -"plugins": { - "allure": {} -} -``` - -Run tests with allure plugin enabled: - - npx codeceptjs run --plugins allure - -By default, allure reports are saved to `output` directory. -Launch Allure server and see the report like on a screenshot above: - - allure serve output - -#### Configuration - -- `outputDir` - a directory where allure reports should be stored. Standard output directory is set by default. -- `enableScreenshotDiffPlugin` - a boolean flag for add screenshot diff to report. - To attach, tou need to attach three files to the report - "diff.png", "actual.png", "expected.png". - See [Allure Screenshot Plugin][5] - -#### Public API - -There are few public API methods which can be accessed from other plugins. - -```js -const allure = codeceptjs.container.plugins('allure'); -``` - -`allure` object has following methods: - -- `addAttachment(name, buffer, type)` - add an attachment to current test / suite -- `addLabel(name, value)` - adds a label to current test -- `addParameter(kind, name, value)` - adds a parameter to current test -- `createStep(name, stepFunc)` - create a step, stepFunc could consist an attachment - Example of usage: - -```js - allure.createStep('New created step', () => { - allure.addAttachment( - 'Request params', - '{"clientId":123, "name":"Tom", "age":29}', - 'application/json' - ); - }); -``` - -![Created Step Image][6] - -- `addScreenDiff(name, expectedImg, actualImg, diffImg)` - add a special screen diff block to the current test case - image must be a string representing the contents of the expected image file encoded in base64 - Example of usage: - -```js -const expectedImg = fs.readFileSync('expectedImg.png', { encoding: 'base64' }); -... -allure.addScreenDiff('Screen Diff', expectedImg, actualImg, diffImg); -``` - -![Overlay][7] -![Diff][8] - -- `severity(value)` - adds severity label -- `epic(value)` - adds epic label -- `feature(value)` - adds feature label -- `story(value)` - adds story label -- `issue(value)` - adds issue label -- `setDescription(description, type)` - sets a description - -### Parameters - -- `config` - -## allure - -Creates an instance of the allure reporter - -### Parameters - -- `config` **Config** Configuration for the allure reporter (optional, default `{outputDir:global.output_dir}`) - -Returns **[Object][9]** Instance of the allure reporter - ## autoDelay Sometimes it takes some time for a page to respond to user's actions. @@ -533,7 +372,7 @@ Possible config options: ## customLocator -Creates a [custom locator][10] by using special attributes in HTML. +Creates a [custom locator][1] by using special attributes in HTML. If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests, you can enable this plugin to simplify matching elements with these attributes: @@ -683,9 +522,9 @@ This method works with WebDriver, Playwright, Puppeteer, Appium helpers. Function parameter `el` represents a matched element. Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list: -- [Playwright ElementHandle][11] -- [Puppeteer][12] -- [webdriverio element][13] +- [Playwright ElementHandle][2] +- [Puppeteer][3] +- [webdriverio element][4] #### Configuration @@ -699,17 +538,17 @@ const eachElement = codeceptjs.container.plugins('eachElement'); ### Parameters -- `purpose` **[string][1]** +- `purpose` **[string][5]** - `locator` **CodeceptJS.LocatorOrString** -- `fn` **[Function][3]** +- `fn` **[Function][6]** -Returns **([Promise][14]<any> | [undefined][15])** +Returns **([Promise][7]<any> | [undefined][8])** ## fakerTransform -Use the [faker.js][16] package to generate fake data inside examples on your gherkin tests +Use the [faker.js][9] package to generate fake data inside examples on your gherkin tests -![Faker.js][17] +![Faker.js][10] #### Usage @@ -747,7 +586,7 @@ Scenario Outline: ... ## pauseOnFail -Automatically launches [interactive pause][18] when a test fails. +Automatically launches [interactive pause][11] when a test fails. Useful for debugging flaky tests on local environment. Add this plugin to config file: @@ -763,20 +602,6 @@ Enable it manually on each run via `-p` option: npx codeceptjs run -p pauseOnFail -## reporter - -Type: Allure - -### pendingCase - -Mark a test case as pending - -#### Parameters - -- `testName` **[string][1]** Name of the test case -- `timestamp` **[number][19]** Timestamp of the test case -- `opts` **[Object][9]** Options for the test case (optional, default `{}`) - ## retryFailedStep Retries each failed step in a test. @@ -944,14 +769,14 @@ Possible config options: ## selenoid -[Selenoid][20] plugin automatically starts browsers and video recording. +[Selenoid][12] plugin automatically starts browsers and video recording. Works with WebDriver helper. ### Prerequisite This plugin **requires Docker** to be installed. -> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][21] tool from Selenoid +> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][13] tool from Selenoid ### Usage @@ -980,7 +805,7 @@ plugins: { } ``` -When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][22] and start Selenoid automatically. +When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][14] and start Selenoid automatically. It will also create `browsers.json` file required by Selenoid. In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file. @@ -992,10 +817,10 @@ In automatic mode the latest version of browser will be used for tests. It is re While this plugin can create containers for you for better control it is recommended to create and launch containers manually. This is especially useful for Continous Integration server as you can configure scaling for Selenoid containers. -> Use [Selenoid Configuration Manager][21] to create and start containers semi-automatically. +> Use [Selenoid Configuration Manager][13] to create and start containers semi-automatically. 1. Create `browsers.json` file in the same directory `codecept.conf.js` is located - [Refer to Selenoid documentation][23] to know more about browsers.json. + [Refer to Selenoid documentation][15] to know more about browsers.json. _Sample browsers.json_ @@ -1020,7 +845,7 @@ _Sample browsers.json_ 2. Create Selenoid container -Run the following command to create a container. To know more [refer here][24] +Run the following command to create a container. To know more [refer here][16] ```bash docker create \ @@ -1053,7 +878,7 @@ When `allure` plugin is enabled a video is attached to report automatically. | enableVideo | Enable video recording and use `video` folder of output (default: false) | | enableLog | Enable log recording and use `logs` folder of output (default: false) | | deletePassed | Delete video and logs of passed tests (default : true) | -| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][25] to know more | +| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][17] to know more | ### Parameters @@ -1061,7 +886,7 @@ When `allure` plugin is enabled a video is attached to report automatically. ## stepByStepReport -![step-by-step-report][26] +![step-by-step-report][18] Generates step by step report for a test. After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow. @@ -1242,7 +1067,7 @@ This plugin allows to run webdriverio services like: - browserstack - appium -A complete list of all available services can be found on [webdriverio website][27]. +A complete list of all available services can be found on [webdriverio website][19]. #### Setup @@ -1254,7 +1079,7 @@ See examples below: #### Selenium Standalone Service -Install `@wdio/selenium-standalone-service` package, as [described here][28]. +Install `@wdio/selenium-standalone-service` package, as [described here][20]. It is important to make sure it is compatible with current webdriverio version. Enable `wdio` plugin in plugins list and add `selenium-standalone` service: @@ -1273,7 +1098,7 @@ Please note, this service can be used with Protractor helper as well! #### Sauce Service -Install `@wdio/sauce-service` package, as [described here][29]. +Install `@wdio/sauce-service` package, as [described here][21]. It is important to make sure it is compatible with current webdriverio version. Enable `wdio` plugin in plugins list and add `sauce` service: @@ -1303,60 +1128,44 @@ In the same manner additional services from webdriverio can be installed, enable - `config` -[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String - -[2]: https://nodejs.org/api/buffer.html - -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function - -[4]: https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png - -[5]: https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md - -[6]: https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png - -[7]: https://user-images.githubusercontent.com/63167966/215404458-9a325668-819e-4289-9b42-5807c49ebddb.png - -[8]: https://user-images.githubusercontent.com/63167966/215404645-73b09da0-9e6d-4352-a123-80c22f7014cd.png - -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[1]: https://codecept.io/locators#custom-locators -[10]: https://codecept.io/locators#custom-locators +[2]: https://playwright.dev/docs/api/class-elementhandle -[11]: https://playwright.dev/docs/api/class-elementhandle +[3]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle -[12]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle +[4]: https://webdriver.io/docs/api -[13]: https://webdriver.io/docs/api +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function -[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[16]: https://www.npmjs.com/package/faker +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[17]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png +[9]: https://www.npmjs.com/package/faker -[18]: /basics/#pause +[10]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png -[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[11]: /basics/#pause -[20]: https://aerokube.com/selenoid/ +[12]: https://aerokube.com/selenoid/ -[21]: https://aerokube.com/cm/latest/ +[13]: https://aerokube.com/cm/latest/ -[22]: https://hub.docker.com/u/selenoid +[14]: https://hub.docker.com/u/selenoid -[23]: https://aerokube.com/selenoid/latest/#_prepare_configuration +[15]: https://aerokube.com/selenoid/latest/#_prepare_configuration -[24]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container +[16]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container -[25]: https://docs.docker.com/engine/reference/commandline/create/ +[17]: https://docs.docker.com/engine/reference/commandline/create/ -[26]: https://codecept.io/img/codeceptjs-slideshow.gif +[18]: https://codecept.io/img/codeceptjs-slideshow.gif -[27]: https://webdriver.io +[19]: https://webdriver.io -[28]: https://webdriver.io/docs/selenium-standalone-service.html +[20]: https://webdriver.io/docs/selenium-standalone-service.html -[29]: https://webdriver.io/docs/sauce-service.html +[21]: https://webdriver.io/docs/sauce-service.html diff --git a/docs/reports.md b/docs/reports.md index 8bdd60a9a..bf444dfb3 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -183,62 +183,6 @@ plugins: { [Testomat.io](https://testomat.io) reporter works in the cloud, so it doesn't require you to install additional software. It can be integrated with your CI service to rerun only failed tests, launch new runs from UI, and send report notifications by email or in Slack, MS Teams, or create issue in Jira. -## Allure - - -[Allure reporter](https://allure.qatools.ru/#) is a tool to store and display test reports. -It provides nice web UI which contains all important information on test execution. -CodeceptJS has built-in support for Allure reports. Inside reports you will have all steps, substeps and screenshots. - -![](https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png) - -> â–ļ Allure is a standalone tool. Please refer to [Allure documentation](https://docs.qameta.io/allure/) to learn more about using Allure reports. - -Allure requires **Java 8** to work. Then Allure can be installed via NPM: - -``` -npm install -g allure-commandline --save-dev -``` - -Add [Allure plugin](/plugins/#allure) in config under `plugins` section. - -```js -plugins: { - allure: { - } -} -``` - -Run tests with allure plugin enabled: - -``` -npx codeceptjs run --plugins allure -``` - -(optionally) To enable allure plugin permanently include `"enabled": true` into plugin config: - - -```js -"plugins": { - "allure": { - "enabled": true - } -} -``` - -Launch Allure server and see the report like on a screenshot above: - -``` -allure serve output -``` - -Allure reporter aggregates data from other plugins like [*stepByStepReport*](/plugins/#stepByStepReport) and [*screenshotOnFail*](/plugins/#screenshotOnFail) - -Allure reports can also be generated for `dry-run` command. So you can get the first report with no tests actually being executed. Enable allure plugin in dry-run options, and pass `--debug` option to print all tests on screen. - -``` -npx codeceptjs dry-run --debug -p allure -``` ## ReportPortal diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 000000000..048edadcf --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,271 @@ +--- +permalink: /tutorial +title: CodeceptJS Testing Tutorial: Create a Complete Test Suite +--- + +**[CodeceptJS](https://codecept.io) is a popular open-source testing framework** for JavaScript. It is designed to simplify writing and maintain end-to-end tests for web applications, using a readable and intuitive syntax. To run tests in browser it uses **[Playwright](https://playwright.dev)** library from Microsoft. + +CodeceptJS was started in 2015 and is widely used by organizations of all sizes, from startups to large enterprises. + +## Let's get CodeceptJS installed! + +To install CodeceptJS, you will need to have Node.js and npm (the Node.js package manager) installed on your system. You can check if you already have these tools installed by running the following commands in a terminal: + +```bash +node --version +npm --version +``` + +If either of these commands return an error, you will need to install Node.js and npm before you can install CodeceptJS. You can download and install the latest version of Node.js from the official website, which includes npm. + +To install CodeceptJS create a new folder and run command form terminal: + +``` +npx create-codeceptjs . +``` + +If you run the npx create-codeceptjs . command, it will install CodeceptJS with Playwright in the current directory. + +> The `npx` command is a tool that comes with npm (the Node.js package manager) and it allows you to run npm packages without having to install them globally on your system. + +It may take some time as it downloads browsers: Chrome, Firefox and Safari and creates a demo project. + +But we are here to write a checkout test, right? + +Let's initialize a new project for that! + +Run + +``` +npx codeceptjs init +``` +Agree on defaults (press Enter for every question asked). When asked for base site URL, provide a URL of a ecommerce website you are testing. For instance, it could be: `https://myshop.com` if you test already published website or `http://localhost` if you run the website locally. + +When asked for a test name and suite name write "Checkout". It will create the following dirctory structure: + +``` +. +├── codecept.conf.js +├── package.json +└── Checkout_test.js +``` + +The `codecept.conf.js` file in the root of the project directory contains the global configuration settings for CodeceptJS. + +Now open a test: + +```js +Feature('Checkout'); + +Scenario('test something', ({ I }) => { +}); +``` +Inside the Scenario block you write a test. + +Add `I.amOnPage('/')` into it. It will open the browser on a URL you specified as a base. + +```js +Feature('Checkout'); + +Scenario('test something', ({ I }) => { + I.amOnPage('/') +}); +``` +But you may want to ask... + +## What is I? + +Glad you asked! + +In CodeceptJS, the `I` object is used to represent the user performing actions in a test scenario. It provides a number of methods (also known as actions) that can be used to simulate user interactions with the application under test. + +Some of the most popular actions of the I object are: + +* `I.amOnPage(url)`: This action navigates the user to the specified URL. +* `I.click(locator)`: This action simulates a click on the element identified by the given locator. +* `I.fillField(field, value)`: This action fills the specified field with the given value. +* `I.see(text, context)`: This action checks that the given text is visible on the page (or in the specified context). +* `I.selectOption(select, option)`: This action selects the specified option from the given select dropdown. +* `I.waitForElement(locator, timeout)`: This action waits for the specified element to appear on the page, up to the given timeout. +* `I.waitForText(text, timeout, context)`: This action waits for the given text to appear on the page (or in the specified context), up to the given timeout. + +We will need to use them to navigate into Checkout process. How do we navigate web? Sure by clicking on links! + +Let's use `I.click()` for that. + +But how we can access elements on a webpage? + +CodeceptJS is smart enough to locate clickable elements by their visible text. For instance, if on your ecommerce website you have a product 'Coffee Cup' with that exact name you can use + +```js +I.click('Coffee Cup'); +``` + +But sometimes elements are not as easy to locate, so you can use CSS or XPath locators to locate them. + +For instance, locating Coffee Cup via CSS can take into accont HTML structure of a page and element attributes. For instance, it can be like this: + +```js +I.click('div.products a.product-name[title="Coffee Cup"]'); +``` + +In this example, the `div.products` part of the selector specifies a div element with the `products` class, and the `a.product-name[title="Coffee Cup"]` part specifies an a element with `the product-name` class and the `title` attribute set to Coffee Cup. + +You can read more about HTML and CSS locators, and basically that's all what you need to know to start writing a checkout test! + +## Get back to Checkout + +Let's see how a regular checkout script may look in CodeceptJS: + +```js +Scenario('test the checkout form', async ({ I }) => { + // we select one product and switched to checkout project + I.amOnPage('/'); + I.click('Coffee Cup'); + I.click('Purchase'); + I.click('Checkout'); + + // fill in the shipping address + I.fillField('First Name', 'John'); + I.fillField('Last Name', 'Doe'); + I.fillField('Address', '123 Main St.'); + I.fillField('City', 'New York'); + I.selectOption('State', 'New York'); + I.fillField('Zip Code', '10001'); + + // select a payment method + I.click('#credit-card-option'); + I.fillField('Card Number', '1234-5678-9012-3456'); + I.fillField('Expiration Date', '12/22'); + I.fillField('Security Code', '123'); + + // click the checkout button + I.click('Checkout'); + + // verify that the checkout was successful + I.see('Your order has been placed successfully!'); +}); +``` +Sure, in relaity your script might be more complicated. As you have noticed, we used CSS locator `'#credit-card-option'` to get select a payment option. However, the test is simple and you can follow user steps through it. + +Please note, that you shouldn't use a real credit card number here. Good news, you don't need to. Payment providers like Strip provide dummy card numbers for testing purposes. + +Run the test with next command: + +``` +npx codeceptjs run --debug -p pauseOnFail +``` + +What are special options here? + +* `--debug` flag is used to output additional information to the console, such as the details of each step in the test, the values of variables, and the results of test assertions. This can help you to identify and fix any issues in your tests. +* `-p pauseOnFail` option is also used to keep the browser opened even if a test fails. It will help us to identify to which point test was executed and what can be improved. + +Add more test steps if needed, update locators, and notify business owners that all that purchases are made by you so your collegues won't call you in the night asking when you want to get a coffee cup 😀 Also the good idea is to run tests on staging website, to not interfere with business process. + +What a test is complete you can run it with: + +``` +npx codeceptjs run +``` + +If you are annoyed to see a browser window you can use `HEADLESS` environment variable: + +``` +HEADLESS=true codeceptjs run +``` +for Windows users HEADLESS should be set in a different manner: + +``` +set HEADLESS=true&& codeceptjs run +``` +The tests will pass but no browser is shown, so you can watch YouTube videos while it goes! + +## Refactoring + +What if you need to check more purchases? Should you copy paste your code for that? + +No! You can use Page Object pattern to put repeating interactions into the reusable functions. + +You can create a page object via next command: + +``` +npx codeceptjs gpo +``` + +Sure, we will call it `Checkout`. It will be created in `./pages/Checkout.js` file. You should enable it in `codecept.conf.js` inside `include` section: + +```js + include: { + ... + checkoutPage: './pages/Checkout.js', + }, + +``` +Now open this file: + +```js +const { I } = inject(); + +module.exports = { + + // insert your locators and methods here +} +``` + +Feels really empty. What should we do about it? Should we write more code? No, we already have it. Let's copy code blocks from a test we have it and place them under a corredponnding function names: + +```js +connst { I } = inject(); + +module.exports = { + + fillShippingAddress(name, address, city, state, zip) { + I.fillField('Name', name); + I.fillField('Address', address); + I.fillField('City', city); + I.fillField('State', state); + I.fillField('Zip', zip); + }, + + fillValidCreditCard() { + I.click('#credit-card-option'); + I.fillField('Card Number', '1234-5678-9012-3456'); + I.fillField('Expiration Date', '12/22'); + I.fillField('Security Code', '123'); + }, + + checkout() { + I.click('Checkout'); + }, +} +``` + +After that we can update our test to use the created page object. Note, that we import Checkout PageObject by its name `checkoutPage` we previously defined in a config. + +```js +Scenario('test the checkout form', async ({I, checkoutPage}) => { + I.amOnPage('/'); + I.click('Coffee Cup'); + I.click('Purchase'); + I.click('Checkout'); + + // fill in the shipping address using the page object + checkoutPage.fillShippingAddress('John', 'Doe', '123 Main St.', 'New York', 'New York', '10001'); + checkoutPage.fillValidCreditCard(); + checkoutPage.checkout(); + + // verify that the checkout was successful + I.see('Your order has been placed successfully!'); +}); +``` + +As you see the code of a test was reduced. And we can write the similar tests on the same manner. + +By applying more and more cases you can test a website to all behaviors. + +## Summary + +This was a deep dive! If you think on just starting test automation, CodeceptJS is the best choice for you as it uses native language to pass commands to browser. + +If you already skilled in JavaScript, with CodeceptJS you can focus on business level of your test, instead of writing code for browser. This way you can keep your tests stable and maintainable. diff --git a/docs/typescript.md b/docs/typescript.md index ef1c6861a..1a100c025 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -45,12 +45,6 @@ If a config file is set in TypeScript format (`codecept.conf.ts`) package `ts-no ## Promise-Based Typings -If you plan to write tests in TypeScript you will probably want to enable "promise-based typings" as you will be asked in `init` command about it: - -```js -? Would you prefer to use promise-based typings and explicitly use `await` for all I.* commands? -``` - By default, CodeceptJS tests are written in synchronous mode. This is a regular CodeceptJS test: ```js @@ -82,7 +76,7 @@ Otherwise they will still return promises but it won't be relfected in type defi To introduce promise-based typings into a current project edit `codecept.conf.ts`: ```ts - fullPromiseBased: true; +fullPromiseBased: true; ``` and rebuild type definitions with diff --git a/examples/codecept.config.js b/examples/codecept.config.js index 9d171ee50..a31e21235 100644 --- a/examples/codecept.config.js +++ b/examples/codecept.config.js @@ -1,6 +1,5 @@ exports.config = { output: './output', - retry: 3, helpers: { Playwright: { url: 'http://localhost', diff --git a/lib/cli.js b/lib/cli.js index 83da4c6e5..c4d59f74b 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -40,7 +40,7 @@ class Cli extends Base { runner.on('fail', (test) => { if (test.ctx.currentTest) { - this.loadedTests.push(test.ctx.currentTest.id); + this.loadedTests.push(test.ctx.currentTest.uid); } if (showSteps && test.steps) { return output.scenario.failed(test); @@ -57,7 +57,7 @@ class Cli extends Base { } else { skipTestConfig(test, null); } - this.loadedTests.push(test.id); + this.loadedTests.push(test.uid); cursor.CR(); output.test.skipped(test); }); @@ -111,7 +111,7 @@ class Cli extends Base { let skippedCount = 0; const grep = runner._grep; for (const test of suite.tests) { - if (!test.state && !this.loadedTests.includes(test.id)) { + if (!test.state && !this.loadedTests.includes(test.uid)) { if (matchTest(grep, test.title)) { if (!test.opts) { test.opts = {}; diff --git a/lib/codecept.js b/lib/codecept.js index b79c814cd..7f9fa332e 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -67,8 +67,8 @@ class Codecept { global.output_dir = fsPath.resolve(dir, this.config.output); if (!this.config.noGlobals) { + global.Helper = global.codecept_helper = require('@codeceptjs/helper'); global.actor = global.codecept_actor = require('./actor'); - global.Helper = global.codecept_helper = require('./helper'); global.pause = require('./pause'); global.within = require('./within'); global.session = require('./session'); diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js index cae52844b..e0f92e11a 100644 --- a/lib/command/gherkin/snippets.js +++ b/lib/command/gherkin/snippets.js @@ -1,6 +1,7 @@ const escapeStringRegexp = require('escape-string-regexp'); const fs = require('fs'); -const { Parser } = require('gherkin'); +const Gherkin = require('@cucumber/gherkin'); +const Messages = require('@cucumber/messages'); const glob = require('glob'); const fsPath = require('path'); @@ -9,7 +10,10 @@ const Codecept = require('../../codecept'); const output = require('../../output'); const { matchStep } = require('../../interfaces/bdd'); -const parser = new Parser(); +const uuidFn = Messages.IdGenerator.uuid(); +const builder = new Gherkin.AstBuilder(uuidFn); +const matcher = new Gherkin.GherkinClassicTokenMatcher(); +const parser = new Gherkin.Parser(builder, matcher); parser.stopAtFirstError = false; module.exports = function (genPath, options) { @@ -85,8 +89,8 @@ module.exports = function (genPath, options) { const parseFile = (file) => { const ast = parser.parse(fs.readFileSync(file).toString()); for (const child of ast.feature.children) { - if (child.type === 'ScenarioOutline') continue; // skip scenario outline - parseSteps(child.steps).map((step) => { + if (child.scenario.keyword === 'Scenario Outline') continue; // skip scenario outline + parseSteps(child.scenario.steps).map((step) => { return Object.assign(step, { file: file.replace(global.codecept_dir, '').slice(1) }); }).map((step) => newSteps.set(`${step.type}(${step})`, step)); } diff --git a/lib/command/init.js b/lib/command/init.js index 298a9f4cf..310bdd27b 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -126,13 +126,6 @@ module.exports = function (initPath) { default: './output', message: 'Where should logs, screenshots, and reports to be stored?', }, - { - name: 'promise', - type: 'confirm', - default: false, - message: 'Would you prefer to use promise-based typings for all I.* commands? http://bit.ly/3XIMq6n', - when: (answers) => answers.typescript, - }, { name: 'translation', type: 'list', @@ -155,7 +148,6 @@ module.exports = function (initPath) { config.tests = result.tests; if (isTypeScript) { config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`; - config.fullPromiseBased = result.promise; } // create a directory tests if it is included in tests path diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index 9dafc4a97..5ffcc3722 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -1,16 +1,10 @@ // For Node version >=10.5.0, have to use experimental flag -const { satisfyNodeVersion } = require('./utils'); const { tryOrDefault } = require('../utils'); const output = require('../output'); const event = require('../event'); const Workers = require('../workers'); module.exports = async function (workerCount, options) { - satisfyNodeVersion( - '>=11.7.0', - 'Required minimum Node version of 11.7.0 to work with "run-workers"', - ); - process.env.profile = options.profile; const { config: testConfig, override = '' } = options; @@ -46,6 +40,9 @@ module.exports = async function (workerCount, options) { try { await workers.bootstrapAll(); await workers.run(); + } catch (err) { + output.error(err); + process.exit(1); } finally { await workers.teardownAll(); } diff --git a/lib/command/utils.js b/lib/command/utils.js index c3f5bd20d..ac3cf7fd6 100644 --- a/lib/command/utils.js +++ b/lib/command/utils.js @@ -1,6 +1,5 @@ const fs = require('fs'); const path = require('path'); -const semver = require('semver'); const util = require('util'); const mkdirp = require('mkdirp'); @@ -71,15 +70,6 @@ function safeFileWrite(file, contents) { module.exports.safeFileWrite = safeFileWrite; -module.exports.satisfyNodeVersion = ( - version, - failureMessage = `Required Node version: ${version}`, -) => { - if (!semver.satisfies(process.version, version)) { - fail(failureMessage); - } -}; - module.exports.captureStream = (stream) => { let oldStream; let buffer = ''; diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index c1a282fcd..e6e220058 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -68,7 +68,7 @@ function filterTests() { mocha.loadFiles(); for (const suite of mocha.suite.suites) { - suite.tests = suite.tests.filter(test => tests.indexOf(test.id) >= 0); + suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0); } } @@ -124,7 +124,7 @@ function initializeListeners() { return { opts: test.opts || {}, tags: test.tags || [], - id: test.id, + uid: test.uid, workerIndex, retries: test._retries, title: test.title, diff --git a/lib/helper.js b/lib/helper.js index d6251fe3c..3919046d1 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,4 +1,2 @@ // helper class was moved out from this repository to allow extending from base class -// without loading full CodeceptJS package -if (!global.codeceptjs) global.codeceptjs = require('./index'); module.exports = require('@codeceptjs/helper'); diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index 7acadd6ad..12a587c0d 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -1,6 +1,6 @@ const path = require('path'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const REST = require('./REST'); /** diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js index 650addcc3..fa11db94f 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -1,5 +1,4 @@ let webdriverio; -let wdioV4; const fs = require('fs'); const axios = require('axios').default; @@ -137,12 +136,8 @@ class Appium extends Webdriver { this.isRunning = false; this.axios = axios.create(); - this.options = undefined; - this.config = undefined; webdriverio = require('webdriverio'); - // @ts-ignore - (!webdriverio.VERSION || webdriverio.VERSION.indexOf('4') !== 0) ? wdioV4 = false : wdioV4 = true; } _validateConfig(config) { @@ -520,10 +515,6 @@ class Appium extends Webdriver { async removeApp(appId, bundleId) { onlyForApps.call(this, supportedPlatform.android); - if (wdioV4) { - return this.browser.removeApp(bundleId); - } - return this.axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`, @@ -614,19 +605,13 @@ class Appium extends Webdriver { */ async seeOrientationIs(orientation) { onlyForApps.call(this); - let currentOrientation; - - if (wdioV4) { - const res = await this.browser.orientation(); - currentOrientation = res; - } const res = await this.axios({ method: 'get', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`, }); - currentOrientation = res.data.value; + const currentOrientation = res.data.value; return truth('orientation', `to be ${orientation}`).assert(currentOrientation === orientation); } @@ -644,9 +629,6 @@ class Appium extends Webdriver { */ async setOrientation(orientation) { onlyForApps.call(this); - if (wdioV4) { - return this.browser.setOrientation(orientation); - } return this.axios({ method: 'post', @@ -920,9 +902,6 @@ class Appium extends Webdriver { */ async sendDeviceKeyEvent(keyValue) { onlyForApps.call(this, supportedPlatform.android); - if (wdioV4) { - return this.browser.sendKeyEvent(keyValue); - } return this.browser.pressKeyCode(keyValue); } diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js index b0cc5d090..a7d3efb62 100644 --- a/lib/helper/FileSystem.js +++ b/lib/helper/FileSystem.js @@ -2,7 +2,7 @@ const assert = require('assert'); const path = require('path'); const fs = require('fs'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const { fileExists } = require('../utils'); const { fileIncludes } = require('../assert/include'); const { fileEquals } = require('../assert/equal'); diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js index 44bcd56ab..5f9196845 100644 --- a/lib/helper/GraphQL.js +++ b/lib/helper/GraphQL.js @@ -1,5 +1,5 @@ const axios = require('axios').default; -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); /** * GraphQL helper allows to send additional requests to a GraphQl endpoint during acceptance tests. diff --git a/lib/helper/GraphQLDataFactory.js b/lib/helper/GraphQLDataFactory.js index 8ae1b0c98..9a6d802e6 100644 --- a/lib/helper/GraphQLDataFactory.js +++ b/lib/helper/GraphQLDataFactory.js @@ -1,6 +1,6 @@ const path = require('path'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const GraphQL = require('./GraphQL'); /** diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index 2d3b3f44e..5a433a044 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -2,7 +2,7 @@ const assert = require('assert'); const chai = require('chai'); const joi = require('joi'); const chaiDeepMatch = require('chai-deep-match'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); chai.use(chaiDeepMatch); diff --git a/lib/helper/Mochawesome.js b/lib/helper/Mochawesome.js index 7e56d6cd5..826b2f232 100644 --- a/lib/helper/Mochawesome.js +++ b/lib/helper/Mochawesome.js @@ -2,7 +2,7 @@ let addMochawesomeContext; let currentTest; let currentSuite; -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const { clearString } = require('../utils'); class Mochawesome extends Helper { diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js index 91b530add..ff180135c 100644 --- a/lib/helper/Nightmare.js +++ b/lib/helper/Nightmare.js @@ -2,7 +2,7 @@ const path = require('path'); const urlResolve = require('url').resolve; -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const { includes: stringIncludes } = require('../assert/include'); const { urlEquals } = require('../assert/equal'); const { equals } = require('../assert/equal'); diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 9b0f28ca0..fbb24ed9d 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1,7 +1,7 @@ const path = require('path'); const fs = require('fs'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const Locator = require('../locator'); const recorder = require('../recorder'); const stringIncludes = require('../assert/include').includes; diff --git a/lib/helper/Protractor.js b/lib/helper/Protractor.js index 54dc6fbf8..48f3eef85 100644 --- a/lib/helper/Protractor.js +++ b/lib/helper/Protractor.js @@ -6,7 +6,7 @@ let ProtractorExpectedConditions; const path = require('path'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const stringIncludes = require('../assert/include').includes; const { urlEquals, equals } = require('../assert/equal'); const { empty } = require('../assert/empty'); diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 36fe1130d..1187561e2 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -3,7 +3,7 @@ const fs = require('fs'); const fsExtra = require('fs-extra'); const path = require('path'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const Locator = require('../locator'); const recorder = require('../recorder'); const stringIncludes = require('../assert/include').includes; diff --git a/lib/helper/REST.js b/lib/helper/REST.js index c00b5f896..111d3763b 100644 --- a/lib/helper/REST.js +++ b/lib/helper/REST.js @@ -1,7 +1,7 @@ const axios = require('axios').default; +const Helper = require('@codeceptjs/helper'); const Secret = require('../secret'); -const Helper = require('../helper'); const { beautify } = require('../utils'); /** diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js index 02e6c7c73..e63d49e3f 100644 --- a/lib/helper/TestCafe.js +++ b/lib/helper/TestCafe.js @@ -6,6 +6,7 @@ const qrcode = require('qrcode-terminal'); const createTestCafe = require('testcafe'); const { Selector, ClientFunction } = require('testcafe'); +const Helper = require('@codeceptjs/helper'); const ElementNotFound = require('./errors/ElementNotFound'); const testControllerHolder = require('./testcafe/testControllerHolder'); const { @@ -22,7 +23,6 @@ const { xpathLocator, } = require('../utils'); const Locator = require('../locator'); -const Helper = require('../helper'); /** * Client Functions diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 1a1e0ca2e..57c82e2b1 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -4,7 +4,7 @@ const assert = require('assert'); const path = require('path'); const fs = require('fs'); -const Helper = require('../helper'); +const Helper = require('@codeceptjs/helper'); const stringIncludes = require('../assert/include').includes; const { urlEquals, equals } = require('../assert/equal'); const { debug } = require('../output'); diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js index 0aa00d304..e1393cb22 100644 --- a/lib/interfaces/bdd.js +++ b/lib/interfaces/bdd.js @@ -1,4 +1,4 @@ -const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('cucumber-expressions'); +const { CucumberExpression, ParameterTypeRegistry, ParameterType } = require('@cucumber/cucumber-expressions'); const Config = require('../config'); let steps = {}; diff --git a/lib/interfaces/gherkin.js b/lib/interfaces/gherkin.js index 6f77b1e13..254b3af1e 100644 --- a/lib/interfaces/gherkin.js +++ b/lib/interfaces/gherkin.js @@ -1,4 +1,5 @@ -const { Parser } = require('gherkin'); +const Gherkin = require('@cucumber/gherkin'); +const Messages = require('@cucumber/messages'); const { Context, Suite, Test } = require('mocha'); const { matchStep } = require('./bdd'); @@ -8,7 +9,10 @@ const Step = require('../step'); const DataTableArgument = require('../data/dataTableArgument'); const transform = require('../transform'); -const parser = new Parser(); +const uuidFn = Messages.IdGenerator.uuid(); +const builder = new Gherkin.AstBuilder(uuidFn); +const matcher = new Gherkin.GherkinClassicTokenMatcher(); +const parser = new Gherkin.Parser(builder, matcher); parser.stopAtFirstError = false; module.exports = (text, file) => { @@ -46,14 +50,20 @@ module.exports = (text, file) => { step.metaStep = metaStep; }; const fn = matchStep(step.text); - if (step.argument) { - step.argument.parse = () => { - return new DataTableArgument(step.argument); - }; - fn.params.push(step.argument); - if (step.argument.type === 'DataTable') metaStep.comment = `\n${transformTable(step.argument)}`; - if (step.argument.content) metaStep.comment = `\n${step.argument.content}`; + + if (step.dataTable) { + fn.params.push({ + ...step.dataTable, + parse: () => new DataTableArgument(step.dataTable), + }); + metaStep.comment = `\n${transformTable(step.dataTable)}`; + } + + if (step.docString) { + fn.params.push(step.docString); + metaStep.comment = `\n"""\n${step.docString.content}\n"""`; } + step.startTime = Date.now(); step.match = fn.line; event.emit(event.bddStep.before, step); @@ -74,15 +84,15 @@ module.exports = (text, file) => { }; for (const child of ast.feature.children) { - if (child.type === 'Background') { - suite.beforeEach('Before', scenario.injected(async () => runSteps(child.steps), suite, 'before')); + if (child.background) { + suite.beforeEach('Before', scenario.injected(async () => runSteps(child.background.steps), suite, 'before')); continue; } - if (child.type === 'ScenarioOutline') { - for (const examples of child.examples) { + if (child.scenario && child.scenario.keyword === 'Scenario Outline') { + for (const examples of child.scenario.examples) { const fields = examples.tableHeader.cells.map(c => c.value); for (const example of examples.tableBody) { - let exampleSteps = [...child.steps]; + let exampleSteps = [...child.scenario.steps]; const current = {}; for (const index in example.cells) { const placeholder = fields[index]; @@ -95,8 +105,8 @@ module.exports = (text, file) => { return step; }); } - const tags = child.tags.map(t => t.name).concat(examples.tags.map(t => t.name)); - const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim(); + const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name)); + const title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim(); const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current))); test.tags = suite.tags.concat(tags); test.file = file; @@ -105,12 +115,15 @@ module.exports = (text, file) => { } continue; } - const tags = child.tags.map(t => t.name); - const title = `${child.name} ${tags.join(' ')}`.trim(); - const test = new Test(title, async () => runSteps(child.steps)); - test.tags = suite.tags.concat(tags); - test.file = file; - suite.addTest(scenario.test(test)); + + if (child.scenario) { + const tags = child.scenario.tags.map(t => t.name); + const title = `${child.scenario.name} ${tags.join(' ')}`.trim(); + const test = new Test(title, async () => runSteps(child.scenario.steps)); + test.tags = suite.tags.concat(tags); + test.file = file; + suite.addTest(scenario.test(test)); + } } return suite; @@ -130,9 +143,9 @@ function addExampleInTable(exampleSteps, placeholders) { for (const placeholder in placeholders) { steps.map((step) => { step = { ...step }; - if (step.argument && step.argument.type === 'DataTable') { - for (const id in step.argument.rows) { - const cells = step.argument.rows[id].cells; + if (step.dataTable) { + for (const id in step.dataTable.rows) { + const cells = step.dataTable.rows[id].cells; cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder]))); } } diff --git a/lib/listener/exit.js b/lib/listener/exit.js index 5ca04a761..7ccc0e171 100644 --- a/lib/listener/exit.js +++ b/lib/listener/exit.js @@ -6,7 +6,7 @@ module.exports = function () { event.dispatcher.on(event.test.failed, (testOrSuite) => { // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object // is a suite and not a test - const id = testOrSuite.id || (testOrSuite.ctx && testOrSuite.ctx.test.id) || 'empty'; + const id = testOrSuite.uid || (testOrSuite.ctx && testOrSuite.ctx.test.uid) || 'empty'; failedTests.push(id); }); @@ -14,7 +14,7 @@ module.exports = function () { event.dispatcher.on(event.test.passed, (testOrSuite) => { // NOTE When an error happens in one of the hooks (BeforeAll/BeforeEach...) the event object // is a suite and not a test - const id = testOrSuite.id || (testOrSuite.ctx && testOrSuite.ctx.test.id) || 'empty'; + const id = testOrSuite.uid || (testOrSuite.ctx && testOrSuite.ctx.test.uid) || 'empty'; failedTests = failedTests.filter(failed => id !== failed); }); diff --git a/lib/listener/retry.js b/lib/listener/retry.js index 9b5fcf989..38faf7346 100644 --- a/lib/listener/retry.js +++ b/lib/listener/retry.js @@ -55,7 +55,7 @@ module.exports = function () { for (const config of retryConfig) { if (config.grep) { - if (!test.title.includes(config.grep)) continue; + if (!test.fullTitle().includes(config.grep)) continue; } if (config.Scenario) { diff --git a/lib/mochaFactory.js b/lib/mochaFactory.js index affe5691e..5ff6e16a6 100644 --- a/lib/mochaFactory.js +++ b/lib/mochaFactory.js @@ -47,13 +47,13 @@ class MochaFactory { let missingFeatureInFile = []; const seenTests = []; mocha.suite.eachTest(test => { - test.id = genTestId(test); + test.uid = genTestId(test); const name = test.fullTitle(); - if (seenTests.includes(test.id)) { + if (seenTests.includes(test.uid)) { dupes.push(name); } - seenTests.push(test.id); + seenTests.push(test.uid); if (name.slice(0, name.indexOf(':')) === '') { missingFeatureInFile.push(test.file); diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js index cf80d2157..dec5c9038 100644 --- a/lib/plugin/allure.js +++ b/lib/plugin/allure.js @@ -1,405 +1,15 @@ -const Allure = require('allure-js-commons'); - -const event = require('../event'); -const logger = require('../output'); -const { ansiRegExp } = require('../utils'); - -const defaultConfig = { - outputDir: global.output_dir, -}; - -/** - * Allure reporter - * - * ![](https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png) - * - * Enables Allure reporter. - * - * #### Usage - * - * To start please install `allure-commandline` package (which requires Java 8) - * - * ``` - * npm install -g allure-commandline --save-dev - * ``` - * - * Add this plugin to config file: - * - * ```js - * "plugins": { - * "allure": {} - * } - * ``` - * - * Run tests with allure plugin enabled: - * - * ``` - * npx codeceptjs run --plugins allure - * ``` - * - * By default, allure reports are saved to `output` directory. - * Launch Allure server and see the report like on a screenshot above: - * - * ``` - * allure serve output - * ``` - * - * #### Configuration - * - * * `outputDir` - a directory where allure reports should be stored. Standard output directory is set by default. - * * `enableScreenshotDiffPlugin` - a boolean flag for add screenshot diff to report. - * To attach, tou need to attach three files to the report - "diff.png", "actual.png", "expected.png". - * See [Allure Screenshot Plugin](https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md) - * - * #### Public API - * - * There are few public API methods which can be accessed from other plugins. - * - * ```js - * const allure = codeceptjs.container.plugins('allure'); - * ``` - * - * `allure` object has following methods: - * - * * `addAttachment(name, buffer, type)` - add an attachment to current test / suite - * * `addLabel(name, value)` - adds a label to current test - * * `addParameter(kind, name, value)` - adds a parameter to current test - * * `createStep(name, stepFunc)` - create a step, stepFunc could consist an attachment - * Example of usage: - * ```js - * allure.createStep('New created step', () => { - * allure.addAttachment( - * 'Request params', - * '{"clientId":123, "name":"Tom", "age":29}', - * 'application/json' - * ); - * }); - * ``` - * ![Created Step Image](https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png) - * - * * `addScreenDiff(name, expectedImg, actualImg, diffImg)` - add a special screen diff block to the current test case - * image must be a string representing the contents of the expected image file encoded in base64 - * Example of usage: - * ```js - * const expectedImg = fs.readFileSync('expectedImg.png', { encoding: 'base64' }); - * ... - * allure.addScreenDiff('Screen Diff', expectedImg, actualImg, diffImg); - * ``` - * ![Overlay](https://user-images.githubusercontent.com/63167966/215404458-9a325668-819e-4289-9b42-5807c49ebddb.png) - * ![Diff](https://user-images.githubusercontent.com/63167966/215404645-73b09da0-9e6d-4352-a123-80c22f7014cd.png) - * - * * `severity(value)` - adds severity label - * * `epic(value)` - adds epic label - * * `feature(value)` - adds feature label - * * `story(value)` - adds story label - * * `issue(value)` - adds issue label - * * `setDescription(description, type)` - sets a description - * - */ - -/** - * Creates an instance of the allure reporter - * @param {Config} [config={ outputDir: global.output_dir }] - Configuration for the allure reporter - * @returns {Object} Instance of the allure reporter - */ -module.exports = (config) => { - defaultConfig.outputDir = global.output_dir; - config = Object.assign(defaultConfig, config); - - const plugin = {}; - - /** - * @type {Allure} - */ - const reporter = new Allure(); - reporter.setOptions({ targetDir: config.outputDir }); - - let currentMetaStep = []; - let currentStep; - - /** - * Mark a test case as pending - * @param {string} testName - Name of the test case - * @param {number} timestamp - Timestamp of the test case - * @param {Object} [opts={}] - Options for the test case - */ - reporter.pendingCase = function (testName, timestamp, opts = {}) { - reporter.startCase(testName, timestamp); - - plugin.addCommonMetadata(); - if (opts.description) plugin.setDescription(opts.description); - if (opts.severity) plugin.severity(opts.severity); - if (opts.severity) plugin.addLabel('tag', opts.severity); - - reporter.endCase('pending', { message: opts.message || 'Test ignored' }, timestamp); - }; - - /** - * Add an attachment to the current test case - * @param {string} name - Name of the attachment - * @param {Buffer} buffer - Buffer of the attachment - * @param {string} type - MIME type of the attachment - */ - plugin.addAttachment = (name, buffer, type) => { - reporter.addAttachment(name, buffer, type); - }; - - /** - Set description for the current test case - @param {string} description - Description for the test case - @param {string} [type='text/plain'] - MIME type of the description - */ - plugin.setDescription = (description, type) => { - const currentTest = reporter.getCurrentTest(); - if (currentTest) { - currentTest.setDescription(description, type); - } else { - logger.error(`The test is not run. Please use "setDescription" for events: - "test.start", "test.before", "test.after", "test.passed", "test.failed", "test.finish"`); - } - }; - - /** - A method for creating a step in a test case. - @param {string} name - The name of the step. - @param {Function} [stepFunc=() => {}] - The function that should be executed for this step. - @returns {any} - The result of the step function. - */ - plugin.createStep = (name, stepFunc = () => { }) => { - let result; - let status = 'passed'; - reporter.startStep(name); - try { - result = stepFunc(this.arguments); - } catch (error) { - status = 'broken'; - throw error; - } finally { - if (!!result - && (typeof result === 'object' || typeof result === 'function') - && typeof result.then === 'function' - ) { - result.then(() => reporter.endStep('passed'), () => reporter.endStep('broken')); - } else { - reporter.endStep(status); - } - } - return result; - }; - - plugin.createAttachment = (name, content, type) => { - if (typeof content === 'function') { - const attachmentName = name; - const buffer = content.apply(this, arguments); - return createAttachment(attachmentName, buffer, type); - } reporter.addAttachment(name, content, type); - }; - - plugin.severity = (severity) => { - plugin.addLabel('severity', severity); - }; - - plugin.epic = (epic) => { - plugin.addLabel('epic', epic); - }; - - plugin.feature = (feature) => { - plugin.addLabel('feature', feature); - }; - - plugin.story = (story) => { - plugin.addLabel('story', story); - }; - - plugin.issue = (issue) => { - plugin.addLabel('issue', issue); - }; - - /** - Adds a label with the given name and value to the current test in the Allure report - @param {string} name - name of the label to add - @param {string} value - value of the label to add - */ - plugin.addLabel = (name, value) => { - const currentTest = reporter.getCurrentTest(); - if (currentTest) { - currentTest.addLabel(name, value); - } else { - logger.error(`The test is not run. Please use "addLabel" for events: - "test.start", "test.before", "test.after", "test.passed", "test.failed", "test.finish"`); - } - }; - - /** - Adds a parameter with the given kind, name, and value to the current test in the Allure report - @param {string} kind - kind of the parameter to add - @param {string} name - name of the parameter to add - @param {string} value - value of the parameter to add - */ - plugin.addParameter = (kind, name, value) => { - const currentTest = reporter.getCurrentTest(); - if (currentTest) { - currentTest.addParameter(kind, name, value); - } else { - logger.error(`The test is not run. Please use "addParameter" for events: - "test.start", "test.before", "test.after", "test.passed", "test.failed", "test.finish"`); - } - }; - - /** - * Add a special screen diff block to the current test case - * @param {string} name - Name of the screen diff block - * @param {string} expectedImg - string representing the contents of the expected image file encoded in base64 - * @param {string} actualImg - string representing the contents of the actual image file encoded in base64 - * @param {string} diffImg - string representing the contents of the diff image file encoded in base64. - * Could be generated by image comparison lib like "pixelmatch" or alternative - */ - plugin.addScreenDiff = (name, expectedImg, actualImg, diffImg) => { - const screenDiff = { - name, - expected: `data:image/png;base64,${expectedImg}`, - actual: `data:image/png;base64,${actualImg}`, - diff: `data:image/png;base64,${diffImg}`, - }; - reporter.addAttachment(name, JSON.stringify(screenDiff), 'application/vnd.allure.image.diff'); - }; - - plugin.addCommonMetadata = () => { - plugin.addLabel('language', 'javascript'); - plugin.addLabel('framework', 'codeceptjs'); - }; - - event.dispatcher.on(event.suite.before, (suite) => { - reporter.startSuite(suite.fullTitle()); - }); - - event.dispatcher.on(event.suite.before, (suite) => { - for (const test of suite.tests) { - if (test.pending) { - reporter.pendingCase(test.title, null, test.opts.skipInfo); - } - } - }); - - event.dispatcher.on(event.suite.after, () => { - reporter.endSuite(); - }); - - event.dispatcher.on(event.test.before, (test) => { - reporter.startCase(test.title); - plugin.addCommonMetadata(); - if (config.enableScreenshotDiffPlugin) { - const currentTest = reporter.getCurrentTest(); - currentTest.addLabel('testType', 'screenshotDiff'); - } - currentStep = null; - }); - - event.dispatcher.on(event.test.started, (test) => { - const currentTest = reporter.getCurrentTest(); - for (const tag of test.tags) { - currentTest.addLabel('tag', tag); - } - }); - - event.dispatcher.on(event.test.failed, (test, err) => { - if (currentStep) reporter.endStep('failed'); - if (currentMetaStep.length) { - currentMetaStep.forEach(() => reporter.endStep('failed')); - currentMetaStep = []; - } - - err.message = err.message.replace(ansiRegExp(), ''); - if (reporter.getCurrentTest()) { - reporter.endCase('failed', err); - } else { - // this means before suite failed, we should report this. - reporter.startCase(`BeforeSuite of suite ${reporter.getCurrentSuite().name} failed.`); - plugin.addCommonMetadata(); - reporter.endCase('failed', err); - } - }); - - event.dispatcher.on(event.test.passed, () => { - if (currentStep) reporter.endStep('passed'); - if (currentMetaStep.length) { - currentMetaStep.forEach(() => reporter.endStep('passed')); - currentMetaStep = []; - } - reporter.endCase('passed'); - }); - - event.dispatcher.on(event.test.skipped, (test) => { - let loaded = true; - if (test.opts.skipInfo.isFastSkipped) { - loaded = false; - reporter.startSuite(test.parent.fullTitle()); - } - reporter.pendingCase(test.title, null, test.opts.skipInfo); - if (!loaded) { - reporter.endSuite(); - } - }); - - event.dispatcher.on(event.step.started, (step) => { - startMetaStep(step.metaStep); - if (currentStep !== step) { - // In multi-session scenarios, actors' names will be highlighted with ANSI - // escape sequences which are invalid XML values - step.actor = step.actor.replace(ansiRegExp(), ''); - reporter.startStep(step.toString()); - currentStep = step; - } - }); - - event.dispatcher.on(event.step.comment, (step) => { - reporter.startStep(step.toString()); - currentStep = step; - reporter.endStep('passed'); - currentStep = null; - }); - - event.dispatcher.on(event.step.passed, (step) => { - if (currentStep === step) { - reporter.endStep('passed'); - currentStep = null; - } - }); - - event.dispatcher.on(event.step.failed, (step) => { - if (currentStep === step) { - reporter.endStep('failed'); - currentStep = null; - } - }); - - let maxLevel; - function finishMetastep(level) { - const metaStepsToFinish = currentMetaStep.splice(maxLevel - level); - metaStepsToFinish.forEach(() => { - // only if the current step is of type Step, end it. - if (reporter.suites && reporter.suites.length && reporter.suites[0].currentStep && reporter.suites[0].currentStep.constructor.name === 'Step') { - reporter.endStep('passed'); - } - }); - } - - function startMetaStep(metaStep, level = 0) { - maxLevel = level; - if (!metaStep) { - finishMetastep(0); - maxLevel--; - return; - } - - startMetaStep(metaStep.metaStep, level + 1); - - if (metaStep.toString() !== currentMetaStep[maxLevel - level]) { - finishMetastep(level); - currentMetaStep.push(metaStep.toString()); - reporter.startStep(metaStep.toString()); - } - } - - return plugin; +module.exports = () => { + console.log('Allure plugin was moved to @codeceptjs/allure-legacy. Please install it and update your config'); + console.log(); + console.log('npm install @codeceptjs/allure-legacy --save-dev'); + console.log(); + console.log('Then update your config to use it:'); + console.log(); + console.log('plugins: {'); + console.log(' allure: {'); + console.log(' enabled: true,'); + console.log(' require: \'@codeceptjs/allure-legacy\','); + console.log(' }'); + console.log('}'); + console.log(); }; diff --git a/lib/workers.js b/lib/workers.js index 64b9e596e..3d3d1e713 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -1,9 +1,9 @@ /* eslint-disable max-classes-per-file */ -const { EventEmitter } = require('events'); const path = require('path'); const mkdirp = require('mkdirp'); const { Worker } = require('worker_threads'); const { Suite, Test, reporters: { Base } } = require('mocha'); +const { EventEmitter } = require('events'); const ms = require('ms'); const Codecept = require('./codecept'); const MochaFactory = require('./mochaFactory'); @@ -107,8 +107,7 @@ const convertToMochaTests = (testGroup) => { mocha.files = testGroup; mocha.loadFiles(); mocha.suite.eachTest((test) => { - const { id } = test; - group.push(id); + group.push(test.uid); }); mocha.unloadFiles(); } @@ -242,8 +241,7 @@ class Workers extends EventEmitter { mocha.suite.eachTest((test) => { const i = groupCounter % groups.length; if (test) { - const { id } = test; - groups[i].push(id); + groups[i].push(test.uid); groupCounter++; } }); @@ -264,8 +262,7 @@ class Workers extends EventEmitter { const i = indexOfSmallestElement(groups); suite.tests.forEach((test) => { if (test) { - const { id } = test; - groups[i].push(id); + groups[i].push(test.uid); } }); }); diff --git a/package.json b/package.json index 6111763a3..70d4d183a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.3.7", + "version": "3.4.0", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", @@ -35,7 +35,6 @@ }, "repository": "Codeception/codeceptjs", "scripts": { - "build": "tsc -p ./", "json-server": "./node_modules/json-server/bin/index.js test/data/rest/db.json -p 8010 --watch -m test/data/rest/headers.js", "json-server:graphql": "node test/data/graphql/index.js", "lint": "eslint bin/ examples/ lib/ test/ translations/ runok.js", @@ -56,23 +55,23 @@ "dependencies": { "@codeceptjs/configure": "^0.8.0", "@codeceptjs/helper": "^1.0.2", + "@cucumber/cucumber-expressions": "^16", + "@cucumber/gherkin": "^26", + "@cucumber/messages": "^21.0.1", "acorn": "^7.4.1", - "allure-js-commons": "^1.3.2", "arrify": "^2.0.1", - "axios": "^0.21.4", + "axios": "^1.3.3", "chai": "^4.3.6", "chai-deep-match": "^1.2.1", "chalk": "^4.1.2", "commander": "^2.20.3", "cross-spawn": "^7.0.3", "css-to-xpath": "^0.1.0", - "cucumber-expressions": "^6.6.2", "envinfo": "^7.8.1", "escape-string-regexp": "^1.0.3", "figures": "^3.2.0", "fn-args": "^4.0.0", "fs-extra": "^8.1.0", - "gherkin": "^5.1.0", "glob": "^6.0.1", "inquirer": "^6.5.2", "joi": "^17.6.0", @@ -80,16 +79,14 @@ "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", "mkdirp": "^1.0.4", - "mocha": "8.1.3", - "mocha-junit-reporter": "1.23.1", + "mocha": "^8.2.0", + "mocha-junit-reporter": "^1.23.3", "ms": "^2.1.3", "parse-function": "^5.6.4", "promise-retry": "^1.1.1", - "requireg": "^0.2.2", "resq": "^1.10.2", - "semver": "^6.3.0", "sprintf-js": "^1.1.1", - "uuid": "^8.3.2" + "uuid": "^9.0" }, "devDependencies": { "@codeceptjs/detox-helper": "^1.0.2", @@ -118,6 +115,7 @@ "form-data": "^3.0.1", "graphql": "^14.6.0", "husky": "^8.0.1", + "inquirer-test": "^2.0.1", "jsdoc": "^3.6.10", "jsdoc-typeof-plugin": "^1.0.0", "json-server": "^0.10.1", @@ -138,11 +136,10 @@ "typedoc-plugin-markdown": "^3.13.4", "typescript": "^4.8.4", "wdio-docker-service": "^1.5.0", - "webdriverio": "^7.16.14", + "webdriverio": "^8.3.8", "xml2js": "^0.4.23", "xmldom": "^0.1.31", - "xpath": "0.0.27", - "inquirer-test": "^2.0.1" + "xpath": "0.0.27" }, "engines": { "node": ">=8.9.1", diff --git a/test/data/sandbox/codecept.customworker.js b/test/data/sandbox/codecept.customworker.js index 69d76604f..ea40b1c04 100644 --- a/test/data/sandbox/codecept.customworker.js +++ b/test/data/sandbox/codecept.customworker.js @@ -9,12 +9,14 @@ exports.config = { }, }, include: {}, - bootstrap: (done) => { + bootstrap: async () => { process.stdout.write('bootstrap b1+'); - setTimeout(() => { - process.stdout.write('b2'); - done(); - }, 1000); + return new Promise(done => { + setTimeout(() => { + process.stdout.write('b2'); + done(); + }, 100); + }); }, mocha: {}, name: 'sandbox', diff --git a/test/data/sandbox/features/step_definitions/my_steps.js b/test/data/sandbox/features/step_definitions/my_steps.js index beec707e9..1dd5c52de 100644 --- a/test/data/sandbox/features/step_definitions/my_steps.js +++ b/test/data/sandbox/features/step_definitions/my_steps.js @@ -56,6 +56,6 @@ After((test) => { console.log(`-- after ${test.title} --`); }); -Fail(() => { +Fail((test) => { console.log(`-- failed ${test.title} --`); }); diff --git a/test/data/sandbox/workers_helper.js b/test/data/sandbox/workers_helper.js index 26f48a7ae..665ac912a 100644 --- a/test/data/sandbox/workers_helper.js +++ b/test/data/sandbox/workers_helper.js @@ -1,7 +1,7 @@ const assert = require('assert'); const { isMainThread } = require('worker_threads'); -const Helper = require('../../../lib/helper'); +const Helper = require('@codeceptjs/helper'); class Workers extends Helper { seeThisIsWorker() { diff --git a/test/graphql/GraphQLDataFactory_test.js b/test/graphql/GraphQLDataFactory_test.js index d38fce30d..e4e82d8d2 100644 --- a/test/graphql/GraphQLDataFactory_test.js +++ b/test/graphql/GraphQLDataFactory_test.js @@ -5,6 +5,7 @@ const fs = require('fs'); const TestHelper = require('../support/TestHelper'); const GraphQLDataFactory = require('../../lib/helper/GraphQLDataFactory'); +global.codeceptjs = require('../../lib'); const graphql_url = TestHelper.graphQLServerUrl(); diff --git a/test/graphql/GraphQL_test.js b/test/graphql/GraphQL_test.js index 63d93bde6..e27f0898f 100644 --- a/test/graphql/GraphQL_test.js +++ b/test/graphql/GraphQL_test.js @@ -4,6 +4,7 @@ const fs = require('fs'); const TestHelper = require('../support/TestHelper'); const GraphQL = require('../../lib/helper/GraphQL'); const Container = require('../../lib/container'); +global.codeceptjs = require('../../lib'); const graphql_url = TestHelper.graphQLServerUrl(); diff --git a/test/helper/AppiumWeb_test.js b/test/helper/AppiumWeb_test.js index 363186e68..39d7510b0 100644 --- a/test/helper/AppiumWeb_test.js +++ b/test/helper/AppiumWeb_test.js @@ -1,4 +1,5 @@ const Appium = require('../../lib/helper/Appium'); +global.codeceptjs = require('../../lib'); let I; const site_url = 'http://davertmik.github.io'; diff --git a/test/helper/Appium_test.js b/test/helper/Appium_test.js index 50b1b4686..f7100658f 100644 --- a/test/helper/Appium_test.js +++ b/test/helper/Appium_test.js @@ -5,6 +5,7 @@ const path = require('path'); const Appium = require('../../lib/helper/Appium'); const AssertionFailedError = require('../../lib/assert/error'); const fileExists = require('../../lib/utils').fileExists; +global.codeceptjs = require('../../lib'); let app; const apk_path = 'storage:filename=selendroid-test-app-0.17.0.apk'; diff --git a/test/helper/JSONResponse_test.js b/test/helper/JSONResponse_test.js index ac6e9c117..0c92d6d58 100644 --- a/test/helper/JSONResponse_test.js +++ b/test/helper/JSONResponse_test.js @@ -2,6 +2,7 @@ const { expect } = require('chai'); const joi = require('joi'); const JSONResponse = require('../../lib/helper/JSONResponse'); const Container = require('../../lib/container'); +global.codeceptjs = require('../../lib'); const data = { posts: [ diff --git a/test/helper/Nightmare_test.js b/test/helper/Nightmare_test.js index 37407c776..5373ddfc9 100644 --- a/test/helper/Nightmare_test.js +++ b/test/helper/Nightmare_test.js @@ -6,6 +6,7 @@ const TestHelper = require('../support/TestHelper'); const Nightmare = require('../../lib/helper/Nightmare'); const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); +global.codeceptjs = require('../../lib'); let I; let browser; diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 61e408d87..9e274c9d5 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -12,6 +12,7 @@ const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); const FileSystem = require('../../lib/helper/FileSystem'); const { deleteDir } = require('../../lib/utils'); +global.codeceptjs = require('../../lib'); let I; let page; diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index f6ab08c06..c81ee3708 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -10,6 +10,7 @@ const Puppeteer = require('../../lib/helper/Puppeteer'); const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); const FileSystem = require('../../lib/helper/FileSystem'); +global.codeceptjs = require('../../lib'); let I; let browser; diff --git a/test/helper/TestCafe_test.js b/test/helper/TestCafe_test.js index e54e7e549..8d115ce22 100644 --- a/test/helper/TestCafe_test.js +++ b/test/helper/TestCafe_test.js @@ -4,6 +4,7 @@ const assert = require('assert'); const TestHelper = require('../support/TestHelper'); const TestCafe = require('../../lib/helper/TestCafe'); const webApiTests = require('./webapi'); +global.codeceptjs = require('../../lib'); let I; const siteUrl = TestHelper.siteUrl(); @@ -15,6 +16,7 @@ describe('TestCafe', function () { before(() => { global.codecept_dir = path.join(__dirname, '/../data'); global.output_dir = path.join(__dirname, '/../data/output'); + global.codeceptjs = require('../../lib/index'); I = new TestCafe({ url: siteUrl, diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js index 9251e61f0..35eabb815 100644 --- a/test/helper/WebDriver_test.js +++ b/test/helper/WebDriver_test.js @@ -7,6 +7,7 @@ const TestHelper = require('../support/TestHelper'); const WebDriver = require('../../lib/helper/WebDriver'); const AssertionFailedError = require('../../lib/assert/error'); const webApiTests = require('./webapi'); +global.codeceptjs = require('../../lib'); const siteUrl = TestHelper.siteUrl(); let wd; diff --git a/test/rest/ApiDataFactory_test.js b/test/rest/ApiDataFactory_test.js index c6a0e4b15..02546a45f 100644 --- a/test/rest/ApiDataFactory_test.js +++ b/test/rest/ApiDataFactory_test.js @@ -4,6 +4,7 @@ const fs = require('fs'); require('../support/setup'); const TestHelper = require('../support/TestHelper'); const ApiDataFactory = require('../../lib/helper/ApiDataFactory'); +global.codeceptjs = require('../../lib'); const api_url = TestHelper.jsonServerUrl(); diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js index 59d169b27..b77defdc1 100644 --- a/test/rest/REST_test.js +++ b/test/rest/REST_test.js @@ -8,6 +8,7 @@ const REST = require('../../lib/helper/REST'); const Container = require('../../lib/container'); const api_url = TestHelper.jsonServerUrl(); +global.codeceptjs = require('../../lib'); let I; const dbFile = path.join(__dirname, '/../data/rest/db.json'); diff --git a/test/runner/allure_test.js b/test/runner/allure_test.js deleted file mode 100644 index cf95659ab..000000000 --- a/test/runner/allure_test.js +++ /dev/null @@ -1,147 +0,0 @@ -const path = require('path'); -const { exec } = require('child_process'); -const fs = require('fs'); -const assert = require('assert'); -const expect = require('expect'); -const { parseString, Parser } = require('xml2js'); -const { deleteDir } = require('../../lib/utils'); - -const parser = new Parser(); -const runner = path.join(__dirname, '/../../bin/codecept.js'); -const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/allure'); -const codecept_run = `${runner} run`; -const codecept_workers = `${runner} run-workers 2`; -const codecept_run_config = (config, grep) => `${codecept_run} --config ${codecept_dir}/${config} ${grep ? `--grep ${grep}` : ''}`; -const codecept_workers_config = (config, grep) => `${codecept_workers} --config ${codecept_dir}/${config} ${grep ? `--grep ${grep}` : ''}`; - -describe('CodeceptJS Allure Plugin', function () { - this.retries(2); - - beforeEach(() => { - deleteDir(path.join(codecept_dir, 'output/ansi')); - deleteDir(path.join(codecept_dir, 'output/success')); - deleteDir(path.join(codecept_dir, 'output/failed')); - deleteDir(path.join(codecept_dir, 'output/skipped')); - }); - - afterEach(() => { - deleteDir(path.join(codecept_dir, 'output/ansi')); - deleteDir(path.join(codecept_dir, 'output/success')); - deleteDir(path.join(codecept_dir, 'output/failed')); - deleteDir(path.join(codecept_dir, 'output/pageobject')); - }); - - it('should correct save info about page object for xml file', (done) => { - exec(codecept_run_config('codecept.po.js'), (err) => { - const files = fs.readdirSync(path.join(codecept_dir, 'output/pageobject')); - - fs.readFile(path.join(codecept_dir, 'output/pageobject', files[0]), (err, data) => { - parser.parseString(data, (err, result) => { - const testCase = result['ns2:test-suite']['test-cases'][0]['test-case'][0]; - const firstMetaStep = testCase.steps[0].step[0]; - expect(firstMetaStep.name[0]).toEqual('I: openDir "aaa"'); - - const nestedMetaStep = firstMetaStep.steps[0].step[0]; - expect(nestedMetaStep.name[0]).toEqual('I am in path "."'); - expect(testCase.steps[0].step[0].steps.length).toEqual(1); - - expect(testCase.labels[0].label).toEqual([ - { $: { name: 'language', value: 'javascript' } }, - { $: { name: 'framework', value: 'codeceptjs' } }, - ]); - - const secondMetaStep = testCase.steps[0].step[1]; - expect(secondMetaStep.name[0]).toEqual('I see file "allure.conf.js"'); - }); - }); - expect(err).toBeFalsy(); - expect(files.length).toEqual(1); - expect(files[0].match(/\.xml$/)).toBeTruthy(); - done(); - }); - }); - - it('should enable allure reports', (done) => { - exec(codecept_run_config('allure.conf.js'), (err) => { - const files = fs.readdirSync(path.join(codecept_dir, 'output/success')); - expect(err).toBeFalsy(); - expect(files.length).toEqual(1); - expect(files[0].match(/\.xml$/)).toBeTruthy(); - done(); - }); - }); - - it('should create xml file when assert message has ansi symbols', (done) => { - exec(codecept_run_config('failed_ansi.conf.js'), (err) => { - expect(err).toBeTruthy(); - const files = fs.readdirSync(path.join(codecept_dir, 'output/ansi')); - expect(files[0].match(/\.xml$/)).toBeTruthy(); - expect(files.length).toEqual(1); - done(); - }); - }); - - it('should report skipped features', (done) => { - exec(codecept_run_config('skipped_feature.conf.js'), (err, stdout) => { - expect(stdout).toContain('OK | 0 passed, 2 skipped'); - const files = fs.readdirSync(path.join(codecept_dir, 'output/skipped')); - const reports = files.map((testResultPath) => { - expect(testResultPath.match(/\.xml$/)).toBeTruthy(); - return fs.readFileSync(path.join(codecept_dir, 'output/skipped', testResultPath), 'utf8'); - }).join(' '); - expect(reports).toContain('Skipped due to "skip" on Feature.'); - done(); - }); - }); - - it('should report skipped features', (done) => { - exec(codecept_run_config('skipped_feature.conf.js'), (err, stdout) => { - stdout.should.include('OK | 0 passed, 2 skipped'); - const files = fs.readdirSync(path.join(codecept_dir, 'output/skipped')); - const reports = files.map((testResultPath) => { - assert(testResultPath.match(/\.xml$/), 'not a xml file'); - return fs.readFileSync(path.join(codecept_dir, 'output/skipped', testResultPath), 'utf8'); - }).join(' '); - reports.should.include('Skipped due to "skip" on Feature.'); - done(); - }); - }); - - it('should report BeforeSuite errors when executing via run command', (done) => { - exec(codecept_run_config('before_suite_test_failed.conf.js'), (err, stdout) => { - expect(stdout).toContain('FAIL | 0 passed, 1 failed'); - - const files = fs.readdirSync(path.join(codecept_dir, 'output/failed')); - // join all reports together - const reports = files.map((testResultPath) => { - expect(files[0].match(/\.xml$/)).toBeTruthy(); - return fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); - }).join(' '); - expect(reports).toContain('BeforeSuite of suite failing setup test suite: failed.'); - expect(reports).toContain('the before suite setup failed'); - expect(reports).toContain('Skipped due to failure in \'before\' hook'); - done(); - }); - }); - - it('should report BeforeSuite errors when executing via run-workers command', function (done) { - if (parseInt(process.version.match(/\d+/), 10) < 12) { - this.skip(); - } - - exec(codecept_workers_config('before_suite_test_failed.conf.js'), (err, stdout) => { - stdout.should.include('FAIL | 0 passed'); - - const files = fs.readdirSync(path.join(codecept_dir, 'output/failed')); - const reports = files.map((testResultPath) => { - expect(testResultPath.match(/\.xml$/)).toBeTruthy(); - return fs.readFileSync(path.join(codecept_dir, 'output/failed', testResultPath), 'utf8'); - }).join(' '); - expect(reports).toContain('BeforeSuite of suite failing setup test suite: failed.'); - expect(reports).toContain('the before suite setup failed'); - // the line below does not work in workers needs investigating https://github.com/Codeception/CodeceptJS/issues/2391 - // expect(reports).toContain('Skipped due to failure in \'before\' hook'); - done(); - }); - }); -}); diff --git a/test/runner/bdd_test.js b/test/runner/bdd_test.js index 201381587..b1abbc990 100644 --- a/test/runner/bdd_test.js +++ b/test/runner/bdd_test.js @@ -1,4 +1,5 @@ const assert = require('assert'); +const { log } = require('console'); const path = require('path'); const exec = require('child_process').exec; diff --git a/test/runner/init_test.js b/test/runner/init_test.js index 4cfcc106a..a4a7791c7 100644 --- a/test/runner/init_test.js +++ b/test/runner/init_test.js @@ -8,7 +8,7 @@ describe('Init Command', function () { this.timeout(20000); it('steps are showing', async () => { - const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, ENTER, 'y']); + const result = await run([runner, 'init'], ['Y', ENTER, ENTER, DOWN, DOWN, DOWN, ENTER, 'y']); result.should.include('Welcome to CodeceptJS initialization tool'); result.should.include('It will prepare and configure a test environment for you'); result.should.include('Installing to'); @@ -16,6 +16,5 @@ describe('Init Command', function () { result.should.include('Where are your tests located? ./*_test.ts'); result.should.include('What helpers do you want to use? REST'); result.should.include('? Do you want to use JSONResponse helper for assertions on JSON responses?'); - result.should.include('? Would you prefer to use promise-based typings for all I.* commands'); }); }); diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index baa8f4dbb..a51f6e495 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -1,5 +1,12 @@ +const Gherkin = require('@cucumber/gherkin'); +const Messages = require('@cucumber/messages'); const { expect } = require('chai'); -const { Parser } = require('gherkin'); + +const uuidFn = Messages.IdGenerator.uuid(); +const builder = new Gherkin.AstBuilder(uuidFn); +const matcher = new Gherkin.GherkinClassicTokenMatcher(); + +const { log } = require('console'); const Config = require('../../lib/config'); const { Given, @@ -60,7 +67,7 @@ describe('BDD', () => { }); it('should parse gherkin input', () => { - const parser = new Parser(); + const parser = new Gherkin.Parser(builder, matcher); parser.stopAtFirstError = false; const ast = parser.parse(text); // console.log('Feature', ast.feature); @@ -68,7 +75,7 @@ describe('BDD', () => { // console.log('Steps', ast.feature.children[0].steps[0]); expect(ast.feature).is.ok; expect(ast.feature.children).is.ok; - expect(ast.feature.children[0].steps).is.ok; + expect(ast.feature.children[0].scenario.steps).is.ok; }); it('should load step definitions', () => { @@ -372,9 +379,11 @@ describe('BDD', () => { let thenParsedRows; Given('I have the following products :', (products) => { + expect(products.rows.length).to.equal(3); givenParsedRows = products.parse(); }); Then('I should see the following products :', (products) => { + expect(products.rows.length).to.equal(3); thenParsedRows = products.parse(); }); @@ -385,6 +394,7 @@ describe('BDD', () => { ['beer', '9'], ['cookies', '12'], ]; + suite.tests[0].fn(() => { expect(givenParsedRows.rawData).is.deep.equal(expectedParsedDataTable); expect(thenParsedRows.rawData).is.deep.equal(expectedParsedDataTable); diff --git a/test/unit/worker_test.js b/test/unit/worker_test.js index 5238f8b09..792f4dea0 100644 --- a/test/unit/worker_test.js +++ b/test/unit/worker_test.js @@ -1,6 +1,5 @@ const { expect } = require('chai'); const path = require('path'); -const semver = require('semver'); const { Workers, event, recorder } = require('../../lib/index'); describe('Workers', () => { @@ -9,7 +8,6 @@ describe('Workers', () => { }); it('should run simple worker', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); const workerConfig = { by: 'test', testConfig: './test/data/sandbox/codecept.workers.conf.js', @@ -18,8 +16,6 @@ describe('Workers', () => { let failedCount = 0; const workers = new Workers(2, workerConfig); - workers.run(); - workers.on(event.test.failed, () => { failedCount += 1; }); @@ -27,6 +23,8 @@ describe('Workers', () => { passedCount += 1; }); + workers.run(); + workers.on(event.all.result, (status) => { expect(status).equal(false); expect(passedCount).equal(5); @@ -36,8 +34,6 @@ describe('Workers', () => { }); it('should create worker by function', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); - const createTestGroups = () => { const files = [ [path.join(codecept_dir, '/custom-worker/base_test.worker.js')], @@ -75,8 +71,6 @@ describe('Workers', () => { }); it('should run worker with custom config', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); - const workerConfig = { by: 'test', testConfig: './test/data/sandbox/codecept.customworker.js', @@ -99,10 +93,10 @@ describe('Workers', () => { workers.run(); - workers.on(event.test.failed, () => { + workers.on(event.test.failed, (test) => { failedCount += 1; }); - workers.on(event.test.passed, () => { + workers.on(event.test.passed, (test) => { passedCount += 1; }); @@ -115,8 +109,6 @@ describe('Workers', () => { }); it('should able to add tests to each worker', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); - const workerConfig = { by: 'test', testConfig: './test/data/sandbox/codecept.customworker.js', @@ -155,8 +147,6 @@ describe('Workers', () => { }); it('should able to add tests to using createGroupsOfTests', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); - const workerConfig = { by: 'test', testConfig: './test/data/sandbox/codecept.customworker.js', @@ -192,8 +182,6 @@ describe('Workers', () => { }); it('Should able to pass data from workers to main thread and vice versa', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); - const workerConfig = { by: 'test', testConfig: './test/data/sandbox/codecept.customworker.js', @@ -222,7 +210,6 @@ describe('Workers', () => { }); it('should propagate non test events', (done) => { - if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version'); const messages = []; const createTestGroups = () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 292f4a715..2648bb99a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -17,6 +17,32 @@ declare namespace CodeceptJS { path?: string, }; + type RetryConfig = { + /** Filter tests by string or regexp pattern */ + grep: string | RegExp; + /** Number of times to repeat scenarios of a Feature */ + Feature: number; + /** Number of times to repeat scenarios */ + Scenario: number; + /** Number of times to repeat Before hook */ + Before: number; + /** Number of times to repeat After hook */ + After: number; + /** Number of times to repeat BeforeSuite hook */ + BeforeSuite: number; + /** Number of times to repeat AfterSuite hook */ + AfterSuite: number; + }; + + type TimeoutConfig = { + /** Filter tests by string or regexp pattern */ + grep: string | RegExp; + /** Set timeout for a scenarios of a Feature */ + Feature: number; + /** Set timeout for scenarios */ + Scenario: number; + }; + type MainConfig = { /** Pattern to locate CodeceptJS tests. * Allows to enter glob pattern or an Array of patterns to match tests / test file names. @@ -172,8 +198,57 @@ declare namespace CodeceptJS { * ```js * timeout: 20, * ``` + * + * Can be customized to use different timeouts for a subset of tests: + * + * ```js + * timeout: [ + * 10, + * { + * grep: '@slow', + * Scenario: 20 + * } + * ] + * ``` */ - timeout?: number; + timeout?: number | Array | TimeoutConfig; + + /** + * Configure retry strategy for tests + * + * To retry all tests 3 times: + * + * ```js + * retry: 3 + * ``` + * + * To retry only Before hook 3 times: + * + * ```js + * retry: { + * Before: 3 + * } + * ``` + * + * To retry tests marked as flaky 3 times, other 1 time: + * + * ```js + * retry: [ + * { + * Scenario: 1, + * Before: 1 + * }, + * { + * grep: '@flaky', + * Scenario: 3 + * Before: 3 + * } + * ] + * ``` + */ + retry?: number | Array | RetryConfig; + + /** Disable registering global functions (Before, Scenario, etc). Not recommended */ noGlobals?: boolean; /** From b0df2e25a7e5777c7ebe8b10e5a79e681c4ac861 Mon Sep 17 00:00:00 2001 From: Peter Ng Date: Wed, 15 Feb 2023 13:21:08 +0100 Subject: [PATCH 24/30] chore: remove wdio v5 leftover (#3587) --- lib/plugin/wdio.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/plugin/wdio.js b/lib/plugin/wdio.js index 695c4c013..b90d007aa 100644 --- a/lib/plugin/wdio.js +++ b/lib/plugin/wdio.js @@ -1,6 +1,4 @@ const debug = require('debug')('codeceptjs:plugin:wdio'); -const path = require('path'); -const fs = require('fs'); const container = require('../container'); const mainConfig = require('../config'); @@ -100,21 +98,15 @@ module.exports = (config) => { const launchers = []; for (const name of config.services) { - // webdriverio v5 style const Service = safeRequire(`@wdio/${name.toLowerCase()}-service`); if (Service) { if (Service.launcher && typeof Service.launcher === 'function') { const Launcher = Service.launcher; - const version = JSON.parse(fs.readFileSync(path.join(require.resolve('webdriverio'), '/../../', 'package.json')).toString()).version; - if (version.indexOf('5') === 0) { - launchers.push(new Launcher(config)); - } else { - const options = { - logPath: global.output_dir, installArgs: seleniumInstallArgs, args: seleniumArgs, ...wdioOptions, - }; - launchers.push(new Launcher(options, [config.capabilities], config)); - } + const options = { + logPath: global.output_dir, installArgs: seleniumInstallArgs, args: seleniumArgs, ...wdioOptions, + }; + launchers.push(new Launcher(options, [config.capabilities], config)); } if (typeof Service === 'function') { services.push(new Service(config, config.capabilities)); From d89441f118053f85ae0c3512654512187d6c71ab Mon Sep 17 00:00:00 2001 From: davert Date: Thu, 16 Feb 2023 01:33:05 +0200 Subject: [PATCH 25/30] updated docs --- docs/changelog.md | 51 +++++++++++++++++++++++++++++++++++++++++++++-- docs/tutorial.md | 8 ++++---- package.json | 2 +- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index d1d4e3788..c8acd2228 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,53 @@ layout: Section # Releases +## 3.4.0 + +* **Updated to latest mocha and modern Cucumber** +* **Allure plugin moved to [@codeceptjs/allure-legacy](https://github.com/codeceptjs/allure-legacy) package**. This happened because allure-commons package v1 was not updated and caused vulnarabilities. Fixes [#3422](https://github.com/codeceptjs/CodeceptJS/issues/3422). We don't plan to maintain allure v2 plugin so it's up to community to take this initiative. Current allure plugin will print a warning message without interfering the run, so it won't accidentally fail your builds. +* Added ability to **[retry Before](https://codecept.io/basics/#retry-before), BeforeSuite, After, AfterSuite** hooks by **[davertmik](https://github.com/davertmik)**: +```js +Feature('flaky Before & BeforeSuite', { retryBefore: 2, retryBeforeSuite: 3 }) +``` + +* **Flexible [retries configuration](https://codecept.io/basics/#retry-configuration) introduced** by **[davertmik](https://github.com/davertmik)**: + +```js +retry: [ + { + // enable this config only for flaky tests + grep: '@flaky', + Before: 3 // retry Before 3 times + Scenario: 3 // retry Scenario 3 times + }, + { + // retry less when running slow tests + grep: '@slow' + Scenario: 1 + Before: 1 + }, { + // retry all BeforeSuite 3 times + BeforeSuite: 3 + } +] +``` +* **Flexible [timeout configuration](https://codecept.io/advanced/#timeout-configuration)** introduced by **[davertmik](https://github.com/davertmik)**: + +```js +timeout: [ + 10, // default timeout is 10secs + { // but increase timeout for slow tests + grep: '@slow', + Feature: 50 + }, +] +``` + +* JsDoc: Removed promise from `I.say`. See [#3535](https://github.com/codeceptjs/CodeceptJS/issues/3535) by **[danielrentz](https://github.com/danielrentz)** +* **[Playwright]** `handleDownloads` requires now a filename param. See [#3511](https://github.com/codeceptjs/CodeceptJS/issues/3511) by **[PeterNgTr](https://github.com/PeterNgTr)** +* **[WebDriver]** Added support for v8, removed support for webdriverio v5 and lower. See [#3578](https://github.com/codeceptjs/CodeceptJS/issues/3578) by **[PeterNgTr](https://github.com/PeterNgTr)** + + ## 3.3.7 đŸ›Šī¸ Features @@ -37,8 +84,8 @@ layout: Section 📖 Documentation -* Updated [Auickstart](https://codecept.io/quickstart/) with detailed explanation of questions in init -* Added [Translation](/translations/) guide +* Updated [Quickstart](https://codecept.io/quickstart/) with detailed explanation of questions in init +* Added [Translation](/translation/) guide * Updated [TypeScript](https://bit.ly/3XIMq6n) guide for promise-based typings * Reordered guides list on a website diff --git a/docs/tutorial.md b/docs/tutorial.md index 048edadcf..8c3461d85 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1,11 +1,11 @@ --- permalink: /tutorial -title: CodeceptJS Testing Tutorial: Create a Complete Test Suite +title: CoeceptJS Complete Tutorial --- -**[CodeceptJS](https://codecept.io) is a popular open-source testing framework** for JavaScript. It is designed to simplify writing and maintain end-to-end tests for web applications, using a readable and intuitive syntax. To run tests in browser it uses **[Playwright](https://playwright.dev)** library from Microsoft. +# Tutorial: Writing Tests for Checkout Page -CodeceptJS was started in 2015 and is widely used by organizations of all sizes, from startups to large enterprises. +**[CodeceptJS](https://codecept.io) is a popular open-source testing framework** for JavaScript. It is designed to simplify writing and maintain end-to-end tests for web applications, using a readable and intuitive syntax. To run tests in browser it uses **[Playwright](https://playwright.dev)** by default but ca execute tests via WebDriver, Puppeteer or Appium. ## Let's get CodeceptJS installed! @@ -266,6 +266,6 @@ By applying more and more cases you can test a website to all behaviors. ## Summary -This was a deep dive! If you think on just starting test automation, CodeceptJS is the best choice for you as it uses native language to pass commands to browser. +If you think on just starting test automation, CodeceptJS is the best choice for you as it uses native language to pass commands to browser. If you already skilled in JavaScript, with CodeceptJS you can focus on business level of your test, instead of writing code for browser. This way you can keep your tests stable and maintainable. diff --git a/package.json b/package.json index 70d4d183a..06b31615d 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "testcafe": "^2.1.0", "ts-morph": "^3.1.3", "ts-node": "^10.9.1", - "tsd-jsdoc": "https://github.com/englercj/tsd-jsdoc.git", + "tsd-jsdoc": "^2.5.0", "typedoc": "^0.23.10", "typedoc-plugin-markdown": "^3.13.4", "typescript": "^4.8.4", From fc4d9aa702c7f08d2fcab0fbd8d088862086565d Mon Sep 17 00:00:00 2001 From: Michael Bodnarchuk Date: Fri, 17 Feb 2023 02:44:48 +0200 Subject: [PATCH 26/30] updated mocha to 10 (#3594) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06b31615d..ff355e04d 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", "mkdirp": "^1.0.4", - "mocha": "^8.2.0", + "mocha": "^10.2.0", "mocha-junit-reporter": "^1.23.3", "ms": "^2.1.3", "parse-function": "^5.6.4", From 62c51c06f056b904205269985056c001b425a7f0 Mon Sep 17 00:00:00 2001 From: Michael Bodnarchuk Date: Fri, 17 Feb 2023 02:44:57 +0200 Subject: [PATCH 27/30] fixed before hook when retires not executed (#3593) --- lib/scenario.js | 10 ++++++---- .../configs/retryHooks/retry_before_fail_test.js | 9 +++++++++ .../sandbox/configs/retryHooks/retry_helper_test.js | 2 +- test/runner/retry_hooks_test.js | 10 ++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 test/data/sandbox/configs/retryHooks/retry_before_fail_test.js diff --git a/lib/scenario.js b/lib/scenario.js index 416d6e7d0..40f5759a1 100644 --- a/lib/scenario.js +++ b/lib/scenario.js @@ -137,16 +137,18 @@ module.exports.injected = function (fn, suite, hookName) { const opts = suite.opts || {}; const retries = opts[`retry${ucfirst(hookName)}`] || 0; - promiseRetry(async (retry) => { + promiseRetry(async (retry, number) => { try { recorder.startUnlessRunning(); await fn.call(this, getInjectedArguments(fn)); - await recorder.promise(); + await recorder.promise().catch(err => retry(err)); } catch (err) { retry(err); } finally { - recorder.stop(); - recorder.start(); + if (number < retries) { + recorder.stop(); + recorder.start(); + } } }, { retries }) .then(() => { diff --git a/test/data/sandbox/configs/retryHooks/retry_before_fail_test.js b/test/data/sandbox/configs/retryHooks/retry_before_fail_test.js new file mode 100644 index 000000000..d33bed571 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_before_fail_test.js @@ -0,0 +1,9 @@ +Feature('Fail #FailBefore hook', { timeout: 10000 }); + +Before(async ({ I }) => { + I.failIfNotWorks(); +}); + +Scenario('helper hook works', () => { + console.log('not works'); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_helper_test.js b/test/data/sandbox/configs/retryHooks/retry_helper_test.js index adb83a2ab..f9bfb8645 100644 --- a/test/data/sandbox/configs/retryHooks/retry_helper_test.js +++ b/test/data/sandbox/configs/retryHooks/retry_helper_test.js @@ -1,4 +1,4 @@ -Feature('Retry #Helper hooks', { retryBefore: 2 }); +Feature('Retry #Helper hooks', { retryBefore: 3 }); Before(async ({ I }) => { I.failIfNotWorks(); diff --git a/test/runner/retry_hooks_test.js b/test/runner/retry_hooks_test.js index f9c135c85..f91c9441f 100644 --- a/test/runner/retry_hooks_test.js +++ b/test/runner/retry_hooks_test.js @@ -28,4 +28,14 @@ describe('CodeceptJS Retry Hooks', function () { }); }); }); + + it('should finish if retry has not happened', (done) => { + exec(config_run_config('codecept.conf.js', '#FailBefore '), (err, stdout) => { + debug_this_test && console.log(stdout); + expect(stdout).toContain('-- FAILURES'); + expect(stdout).toContain('not works'); + expect(stdout).toContain('1) Fail #FailBefore hook'); + done(); + }); + }); }); From ad5f4afab6e1f5e12065398a7a4e7e94fead33fb Mon Sep 17 00:00:00 2001 From: davert Date: Fri, 17 Feb 2023 02:47:21 +0200 Subject: [PATCH 28/30] version bump --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ff3721a..3ac7766fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.4.1 + +* Updated mocha to v 10.2. Fixes #3591 +* Fixes executing a faling Before hook. Resolves #3592 + ## 3.4.0 * **Updated to latest mocha and modern Cucumber** diff --git a/package.json b/package.json index ff355e04d..a3703ca1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.4.0", + "version": "3.4.1", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", From e41ae9329870829868d776c16ca35fad60ed10b8 Mon Sep 17 00:00:00 2001 From: KobeNguyenT <7845001+kobenguyent@users.noreply.github.com> Date: Sun, 26 Feb 2023 15:31:03 +0100 Subject: [PATCH 29/30] fix: typings issues (#3602) --- typings/index.d.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2648bb99a..da83dbfa9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -19,19 +19,19 @@ declare namespace CodeceptJS { type RetryConfig = { /** Filter tests by string or regexp pattern */ - grep: string | RegExp; + grep?: string | RegExp; /** Number of times to repeat scenarios of a Feature */ - Feature: number; + Feature?: number; /** Number of times to repeat scenarios */ - Scenario: number; + Scenario?: number; /** Number of times to repeat Before hook */ - Before: number; + Before?: number; /** Number of times to repeat After hook */ - After: number; + After?: number; /** Number of times to repeat BeforeSuite hook */ - BeforeSuite: number; + BeforeSuite?: number; /** Number of times to repeat AfterSuite hook */ - AfterSuite: number; + AfterSuite?: number; }; type TimeoutConfig = { @@ -392,7 +392,7 @@ declare namespace CodeceptJS { | { ios: string } | { android: string; ios: string } | { react: string } - | { shadow: string } + | { shadow: string[] } | { custom: string }; interface CustomLocators {} From de8af3347ddb8b78d8539afd31fac0a14176d98b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Mar 2023 23:58:00 +0000 Subject: [PATCH 30/30] 3.3.7-rc.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca8a83e15..bb385c925 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gaainf/codeceptjs", - "version": "3.3.7-rc.6", + "version": "3.3.7-rc.7", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance",