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.
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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bbca6d4d..ed2995767 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,55 @@
+## 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**
+* **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/README.md b/README.md
index 008a269bc..b6033a0f3 100644
--- a/README.md
+++ b/README.md
@@ -98,8 +98,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
@@ -291,36 +290,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/docs/advanced.md b/docs/advanced.md
index d1a9370ac..b5b686918 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,62 @@ 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.
+
-Timeouts can be set globally in config:
+Timeout rules can be set globally via 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/docs/best.md b/docs/best.md
index ca7d468c8..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.
@@ -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/changelog.md b/docs/changelog.md
index fb4e42730..822dde884 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/helpers/ApiDataFactory.md b/docs/helpers/ApiDataFactory.md
index 4e4d24ef7..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
@@ -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/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/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/helpers/Playwright.md b/docs/helpers/Playwright.md
index d84914ea2..c892c90f3 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'.
@@ -1138,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/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/docs/plugins.md b/docs/plugins.md
index 890906f5c..76a0e4182 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -7,83 +7,6 @@ title: Plugins
-## allure
-
-Allure reporter
-
-![][1]
-
-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][2]
-
-#### 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][3]
-
-- `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`
-
## autoDelay
Sometimes it takes some time for a page to respond to user's actions.
@@ -449,7 +372,7 @@ Possible config options:
## customLocator
-Creates a [custom locator][4] 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:
@@ -599,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][5]
-- [Puppeteer][6]
-- [webdriverio element][7]
+- [Playwright ElementHandle][2]
+- [Puppeteer][3]
+- [webdriverio element][4]
#### Configuration
@@ -615,17 +538,17 @@ const eachElement = codeceptjs.container.plugins('eachElement');
### Parameters
-- `purpose` **[string][8]**
+- `purpose` **[string][5]**
- `locator` **CodeceptJS.LocatorOrString**
-- `fn` **[Function][9]**
+- `fn` **[Function][6]**
-Returns **([Promise][10]<any> | [undefined][11])**
+Returns **([Promise][7]<any> | [undefined][8])**
## fakerTransform
-Use the [faker.js][12] 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][13]
+![Faker.js][10]
#### Usage
@@ -663,7 +586,7 @@ Scenario Outline: ...
## pauseOnFail
-Automatically launches [interactive pause][14] 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:
@@ -846,14 +769,14 @@ Possible config options:
## selenoid
-[Selenoid][15] 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][16] tool from Selenoid
+> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][13] tool from Selenoid
### Usage
@@ -882,7 +805,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][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.
@@ -894,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][16] 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][18] to know more about browsers.json.
+ [Refer to Selenoid documentation][15] to know more about browsers.json.
_Sample browsers.json_
@@ -922,7 +845,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][16]
```bash
docker create \
@@ -955,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][20] to know more |
+| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][17] to know more |
### Parameters
@@ -963,7 +886,7 @@ When `allure` plugin is enabled a video is attached to report automatically.
## stepByStepReport
-![step-by-step-report][21]
+![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.
@@ -1144,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][22].
+A complete list of all available services can be found on [webdriverio website][19].
#### Setup
@@ -1156,7 +1079,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][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:
@@ -1175,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][24].
+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:
@@ -1205,50 +1128,44 @@ 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
-
-[2]: https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md
-
-[3]: https://user-images.githubusercontent.com/63167966/139339384-e6e70a62-3638-406d-a224-f32473071428.png
-
-[4]: https://codecept.io/locators#custom-locators
+[1]: https://codecept.io/locators#custom-locators
-[5]: https://playwright.dev/docs/api/class-elementhandle
+[2]: https://playwright.dev/docs/api/class-elementhandle
-[6]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle
+[3]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle
-[7]: https://webdriver.io/docs/api
+[4]: https://webdriver.io/docs/api
-[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
+[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
-[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
+[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
-[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
+[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
-[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
+[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
-[12]: https://www.npmjs.com/package/faker
+[9]: https://www.npmjs.com/package/faker
-[13]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png
+[10]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png
-[14]: /basics/#pause
+[11]: /basics/#pause
-[15]: https://aerokube.com/selenoid/
+[12]: https://aerokube.com/selenoid/
-[16]: https://aerokube.com/cm/latest/
+[13]: https://aerokube.com/cm/latest/
-[17]: https://hub.docker.com/u/selenoid
+[14]: https://hub.docker.com/u/selenoid
-[18]: https://aerokube.com/selenoid/latest/#_prepare_configuration
+[15]: https://aerokube.com/selenoid/latest/#_prepare_configuration
-[19]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container
+[16]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container
-[20]: https://docs.docker.com/engine/reference/commandline/create/
+[17]: https://docs.docker.com/engine/reference/commandline/create/
-[21]: https://codecept.io/img/codeceptjs-slideshow.gif
+[18]: https://codecept.io/img/codeceptjs-slideshow.gif
-[22]: https://webdriver.io
+[19]: https://webdriver.io
-[23]: https://webdriver.io/docs/selenium-standalone-service.html
+[20]: https://webdriver.io/docs/selenium-standalone-service.html
-[24]: 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.
-
-
-
-> âļ 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..8c3461d85
--- /dev/null
+++ b/docs/tutorial.md
@@ -0,0 +1,271 @@
+---
+permalink: /tutorial
+title: CoeceptJS Complete Tutorial
+---
+
+# Tutorial: Writing Tests for Checkout Page
+
+**[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!
+
+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
+
+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 758ccab94..1a100c025 100644
--- a/docs/typescript.md
+++ b/docs/typescript.md
@@ -41,16 +41,10 @@ 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
-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/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..a31e21235 100644
--- a/examples/codecept.config.js
+++ b/examples/codecept.config.js
@@ -14,6 +14,9 @@ exports.config = {
show: !process.env.HEADLESS,
},
REST: {},
+ User: {
+ require: './user_helper.js',
+ },
},
include: {
I: './custom_steps.js',
@@ -60,7 +63,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/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}`, () => {
diff --git a/lib/cli.js b/lib/cli.js
index 4c9ae0a51..acaf63c9f 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 c922812dd..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');
@@ -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/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');
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 c578c39e4..f5f2bf53e 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/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
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 bdf8c7bf6..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');
/**
@@ -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
@@ -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);
}
@@ -262,8 +261,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 +287,8 @@ class ApiDataFactory extends Helper {
*
* @param {*} factory
* @param {*} times
- * @param {*} params
- * @param {*} options
+ * @param {*} [params]
+ * @param {*} [options]
*/
haveMultiple(factory, times, params, options) {
const 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}`);
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 2ac0a9fdf..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;
@@ -131,6 +130,7 @@ class Appium extends Webdriver {
* @augments WebDriver
*/
+ // @ts-ignore
constructor(config) {
super(config);
@@ -138,7 +138,6 @@ class Appium extends Webdriver {
this.axios = axios.create();
webdriverio = require('webdriverio');
- (!webdriverio.VERSION || webdriverio.VERSION.indexOf('4') !== 0) ? wdioV4 = false : wdioV4 = true;
}
_validateConfig(config) {
@@ -516,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`,
@@ -610,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);
}
@@ -640,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',
@@ -916,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 5db102038..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');
@@ -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/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 47b182fa6..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');
/**
@@ -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/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 806e913d4..eb21f76ad 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;
@@ -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'.
@@ -1266,7 +1267,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) {
@@ -2988,7 +2989,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 => {});
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 0bf69f1a1..c983bbf45 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 fe5252d82..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
@@ -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/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 3ffd76943..57c82e2b1 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -4,12 +4,12 @@ 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');
-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;
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/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/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
new file mode 100644
index 000000000..38faf7346
--- /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.fullTitle().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/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 d12c37d3c..dec5c9038 100644
--- a/lib/plugin/allure.js
+++ b/lib/plugin/allure.js
@@ -1,324 +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
- *
- * 
- *
- * 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'
- * );
- * });
- * ```
- * 
- * * `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
- *
- */
-module.exports = (config) => {
- defaultConfig.outputDir = global.output_dir;
- config = Object.assign(defaultConfig, config);
-
- const plugin = {};
-
- const reporter = new Allure();
- reporter.setOptions({ targetDir: config.outputDir });
-
- let currentMetaStep = [];
- let currentStep;
-
- reporter.pendingCase = function (testName, timestamp, opts = {}) {
- reporter.startCase(testName, timestamp);
-
- 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);
- };
-
- plugin.addAttachment = (name, buffer, type) => {
- reporter.addAttachment(name, buffer, type);
- };
-
- 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"`);
- }
- };
-
- 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);
- };
-
- 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"`);
- }
- };
-
- 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"`);
- }
- };
-
- 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);
- 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.`);
- 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/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/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));
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/lib/scenario.js b/lib/scenario.js
index 4e4090d03..40f5759a1 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,33 @@ 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, number) => {
+ try {
+ recorder.startUnlessRunning();
+ await fn.call(this, getInjectedArguments(fn));
+ await recorder.promise().catch(err => retry(err));
+ } catch (err) {
+ retry(err);
+ } finally {
+ if (number < retries) {
+ recorder.stop();
+ recorder.start();
+ }
+ }
+ }, { retries })
+ .then(() => {
recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
recorder.add(`finish ${hookName} hook`, () => done());
recorder.catch();
@@ -146,22 +163,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/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 23561c955..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",
@@ -35,7 +35,6 @@
},
"repository": "gaainf/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,21 +79,19 @@
"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": "^10.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",
"@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",
@@ -118,10 +115,10 @@
"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",
- "mocha-parallel-tests": "^2.3.0",
"nightmare": "^3.0.2",
"nodemon": "^1.19.4",
"playwright": "^1.23.2",
@@ -131,19 +128,18 @@
"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",
+ "tsd-jsdoc": "^2.5.0",
"typedoc": "^0.23.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/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())
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/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_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_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..f9bfb8645
--- /dev/null
+++ b/test/data/sandbox/configs/retryHooks/retry_helper_test.js
@@ -0,0 +1,9 @@
+Feature('Retry #Helper hooks', { retryBefore: 3 });
+
+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/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/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 eef75f43a..a4a7791c7 100644
--- a/test/runner/init_test.js
+++ b/test/runner/init_test.js
@@ -6,61 +6,15 @@ 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, '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?');
});
});
diff --git a/test/runner/retry_hooks_test.js b/test/runner/retry_hooks_test.js
new file mode 100644
index 000000000..f91c9441f
--- /dev/null
+++ b/test/runner/retry_hooks_test.js
@@ -0,0 +1,41 @@
+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();
+ });
+ });
+ });
+
+ 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();
+ });
+ });
+});
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..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', () => {
@@ -273,7 +280,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 +291,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) => {
@@ -366,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();
});
@@ -379,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..da83dbfa9 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;
/**
@@ -317,7 +392,7 @@ declare namespace CodeceptJS {
| { ios: string }
| { android: string; ios: string }
| { react: string }
- | { shadow: string }
+ | { shadow: string[] }
| { custom: string };
interface CustomLocators {}
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