├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS └── workflows │ ├── CI-pipeline.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── index.js ├── jest.config.js ├── lib ├── commands │ ├── reportPortalCommands.d.ts │ └── reportPortalCommands.js ├── constants.js ├── cypressReporter.js ├── ipcEvents.js ├── ipcServer.js ├── mergeLaunches.js ├── mergeLaunchesUtils.js ├── plugin │ ├── index.js │ └── ipcClient.js ├── reporter.js ├── testStatuses.js ├── utils │ ├── attachments.js │ ├── common.js │ ├── index.js │ ├── objectCreators.js │ └── specCountCalculation.js └── worker.js ├── package-lock.json ├── package.json ├── test ├── mergeLaunches.test.js ├── mergeLaunchesUtils.test.js ├── mock │ └── mocks.js ├── reporter.test.js └── utils │ ├── attachments.test.js │ ├── common.test.js │ ├── objectCreators.test.js │ └── specCountCalculation.test.js └── version_fragment /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/* 3 | package.json 4 | package-lock.json 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": ["airbnb-base", "plugin:prettier/recommended", "plugin:cypress/recommended", "plugin:jest/recommended"], 7 | "parserOptions": { 8 | "ecmaVersion": 2020 9 | }, 10 | "plugins": ["prettier", "jest"], 11 | "rules": { 12 | "prettier/prettier": 2, 13 | "no-unused-expressions": [ 14 | "error", 15 | { "allowTernary": true, "allowShortCircuit": true } 16 | ], 17 | "import/prefer-default-export": 0, 18 | "import/no-extraneous-dependencies": 0, 19 | "no-restricted-globals": 0, 20 | "prefer-destructuring": 0, 21 | "import/no-cycle": 0, 22 | "import/named": 0, 23 | "no-else-return": 0, 24 | "lines-between-class-members": 0, 25 | "import/no-useless-path-segments": 0, 26 | "no-invalid-this": 0, 27 | "prefer-object-spread": 0, 28 | "no-console": 0 29 | }, 30 | "overrides": [ 31 | { 32 | "files": ["example/integration/*.js"], 33 | "rules": { 34 | "jest/global": 0 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AmsterGet 2 | -------------------------------------------------------------------------------- /.github/workflows/CI-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: CI-pipeline 15 | 16 | on: 17 | push: 18 | branches: 19 | - develop 20 | - '!master' 21 | paths-ignore: 22 | - README.md 23 | - CHANGELOG.md 24 | pull_request: 25 | paths-ignore: 26 | - README.md 27 | - CHANGELOG.md 28 | 29 | jobs: 30 | test: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | - name: Set up Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: 20 39 | - name: Install dependencies 40 | run: npm install 41 | - name: Run lint 42 | run: npm run lint 43 | - name: Run tests and check coverage 44 | run: npm run test:coverage 45 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: publish 15 | 16 | on: 17 | repository_dispatch: 18 | types: [version-released] 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | - name: Set up Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | - name: Install dependencies 31 | run: npm install 32 | - name: Run lint 33 | run: npm run lint 34 | - name: Run tests and check coverage 35 | run: npm run test:coverage 36 | 37 | publish-to-npm-and-gpr: 38 | needs: build 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | - name: Set up Node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 20 47 | registry-url: 'https://registry.npmjs.org' 48 | - name: Install dependencies 49 | run: npm install 50 | - name: Publish to NPM 51 | run: | 52 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 53 | npm config list 54 | npm publish --access public 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 57 | - name: Set up Node.js 58 | uses: actions/setup-node@v4 59 | with: 60 | node-version: 20 61 | registry-url: 'https://npm.pkg.github.com' 62 | scope: '@reportportal' 63 | - name: Publish to GPR 64 | run: | 65 | npm config set //npm.pkg.github.com/:_authToken=$NODE_AUTH_TOKEN 66 | npm config set scope '@reportportal' 67 | npm config list 68 | npm publish 69 | env: 70 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: release 15 | 16 | on: 17 | push: 18 | branches: 19 | - master 20 | paths-ignore: 21 | - '.github/**' 22 | - README.md 23 | - CHANGELOG.md 24 | 25 | env: 26 | versionFileName: 'VERSION' 27 | versionFragmentFileName: 'version_fragment' 28 | changelogFileName: 'CHANGELOG.md' 29 | jobs: 30 | calculate-version: 31 | runs-on: ubuntu-latest 32 | outputs: 33 | releaseVersion: ${{ steps.exposeVersion.outputs.releaseVersion }} 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | - name: Read version 38 | id: readVersion 39 | run: | 40 | read -r version < ${{ env.versionFileName }} 41 | echo "Snapshot version: $version"; 42 | version=$(echo $version | sed 's/-SNAPSHOT//'); 43 | echo $version; 44 | echo "::set-output name=version::$version" 45 | read -r versionFragment < ${{ env.versionFragmentFileName }} 46 | echo $versionFragment 47 | if [[ "$versionFragment" == "minor" ]]; then 48 | versionFragment=feature 49 | echo "Minor version will be used" 50 | elif [[ "$versionFragment" == "major" ]]; then 51 | echo "Major version will be used" 52 | else 53 | versionFragment=patch 54 | echo "Patch version will be used" 55 | fi 56 | echo "::set-output name=versionFragment::$versionFragment" 57 | - name: Bump release version if needed according to version fragment 58 | if: steps.readVersion.outputs.versionFragment != 'patch' 59 | id: bumpVersion 60 | uses: christian-draeger/increment-semantic-version@1.0.1 61 | with: 62 | current-version: ${{ steps.readVersion.outputs.version }} 63 | version-fragment: ${{ steps.readVersion.outputs.versionFragment }} 64 | - name: Expose release version 65 | id: exposeVersion 66 | run: | 67 | versionFragment=${{ steps.readVersion.outputs.versionFragment }} 68 | if [[ "$versionFragment" != "patch" ]]; then 69 | echo "::set-output name=releaseVersion::${{ steps.bumpVersion.outputs.next-version }}" 70 | else 71 | echo "::set-output name=releaseVersion::${{ steps.readVersion.outputs.version }}" 72 | fi 73 | 74 | create-tag: 75 | needs: calculate-version 76 | runs-on: ubuntu-latest 77 | outputs: 78 | versionInfo: ${{ steps.readChangelogEntry.outputs.log_entry }} 79 | steps: 80 | - name: Checkout repository 81 | uses: actions/checkout@v4 82 | - name: Setup NodeJS 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: '12' 86 | - name: Configure git 87 | run: | 88 | git config --global user.email "reportportal.io" 89 | git config --global user.name "reportportal.io" 90 | git remote set-url origin https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} 91 | - name: Update VERSION file 92 | run: | 93 | echo "${{ needs.calculate-version.outputs.releaseVersion }}" > ${{ env.versionFileName }} 94 | git status 95 | git add ${{ env.versionFileName }} 96 | git commit -m "Update VERSION file with ${{ needs.calculate-version.outputs.releaseVersion }}" 97 | - name: Create tag 98 | run: | 99 | git tag -a v${{ needs.calculate-version.outputs.releaseVersion }} -m ${{ needs.calculate-version.outputs.releaseVersion }} 100 | npm version from-git 101 | git push origin master 102 | - name: Update version in changelog file 103 | run: | 104 | releaseDate=$(date +'%Y-%m-%d') 105 | echo "Release date: $releaseDate" 106 | versionInfo="## [${{ needs.calculate-version.outputs.releaseVersion }}] - $releaseDate" 107 | sed -i '1s/^/\n'"$versionInfo"'\n/' ${{ env.changelogFileName }} 108 | git status 109 | git add ${{ env.changelogFileName }} 110 | git commit -m "Mention ${{ needs.calculate-version.outputs.releaseVersion }} version in changelog file" 111 | git push origin master 112 | - name: Read changelog Entry 113 | id: readChangelogEntry 114 | uses: mindsers/changelog-reader-action@v1.1.0 115 | with: 116 | version: ${{ needs.calculate-version.outputs.releaseVersion }} 117 | path: ./${{ env.changelogFileName }} 118 | - name: Bump snapshot version 119 | id: bumpSnapshotVersion 120 | uses: christian-draeger/increment-semantic-version@1.0.1 121 | with: 122 | current-version: ${{ needs.calculate-version.outputs.releaseVersion }} 123 | version-fragment: 'bug' 124 | - name: Update develop with snapshot version 125 | run: | 126 | git fetch 127 | git checkout develop 128 | git merge master -Xtheirs --allow-unrelated-histories 129 | echo "${{ steps.bumpSnapshotVersion.outputs.next-version }}-SNAPSHOT" > ${{ env.versionFileName }} 130 | echo "patch" > ${{ env.versionFragmentFileName }} 131 | git status 132 | git add ${{ env.versionFileName }} 133 | git add ${{ env.versionFragmentFileName }} 134 | git commit -m "${{ needs.calculate-version.outputs.releaseVersion }} -> ${{ steps.bumpSnapshotVersion.outputs.next-version }}-SNAPSHOT" 135 | git push origin develop 136 | 137 | create-release: 138 | needs: [calculate-version, create-tag] 139 | runs-on: ubuntu-latest 140 | steps: 141 | - name: Checkout repository 142 | uses: actions/checkout@v4 143 | - name: Create Release 144 | id: createRelease 145 | uses: actions/create-release@v1 146 | env: 147 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 148 | with: 149 | tag_name: v${{ needs.calculate-version.outputs.releaseVersion }} 150 | release_name: Release v${{ needs.calculate-version.outputs.releaseVersion }} 151 | body: ${{ needs.create-tag.outputs.versionInfo }} 152 | draft: false 153 | prerelease: false 154 | - name: Trigger the publish workflow 155 | if: success() 156 | uses: peter-evans/repository-dispatch@v1 157 | with: 158 | token: ${{ secrets.GITHUB_TOKEN }} 159 | event-type: version-released 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules/ 8 | 9 | # Editor 10 | .idea/ 11 | .vscode/ 12 | 13 | # Optional npm directory 14 | .npm 15 | .npmrc 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # Mac files 21 | .DS_Store 22 | 23 | # Cypress 24 | cypress/ 25 | cypress.json 26 | 27 | # Example attachments 28 | example/screenshots/ 29 | 30 | # Jest coverage 31 | coverage/ 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [5.5.0] - 2025-03-27 3 | ### Added 4 | - `debugIpc` option. Allows printing logs from the internal node-ipc server and client. Useful for debugging. 5 | - `retryIpcInterval` option. Allows configuring connection retry interval for node-ipc client. 6 | ### Changed 7 | - Revert time format back to milliseconds (based on [#217](https://github.com/reportportal/client-javascript/issues/217#issuecomment-2659843471)). 8 | This is also fixing the issue with agent installation on ARM processors [#212](https://github.com/reportportal/agent-js-cypress/issues/212). 9 | - `@reportportal/client-javascript` bumped to version `5.4.0`. 10 | ### Security 11 | - Updated versions of vulnerable packages (axios). 12 | 13 | ## [5.4.0] - 2024-09-23 14 | ### Changed 15 | - **Breaking change** Drop support of Node.js 12. The version [5.3.5](https://github.com/reportportal/agent-js-cypress/releases/tag/v5.3.5) is the latest that supports it. 16 | - The agent now supports reporting the time for launches, test items and logs with microsecond precision in the ISO string format. 17 | For logs, microsecond precision is available on the UI from ReportPortal version 24.2. 18 | - `@reportportal/client-javascript` bumped to version `5.3.0`. 19 | 20 | ## [5.3.5] - 2024-09-11 21 | ### Added 22 | - `videoCompression` option. Allows compressing Cypress videos before uploading them to the ReportPortal. Check the readme for details. Thanks to [ashvinjaiswal](https://github.com/ashvinjaiswal). 23 | 24 | ## [5.3.4] - 2024-08-29 25 | ### Fixed 26 | - Video attachment won't play after downloading, resolves [#202](https://github.com/reportportal/agent-js-cypress/issues/202). Thanks to [ashvinjaiswal](https://github.com/ashvinjaiswal). 27 | 28 | ## [5.3.3] - 2024-08-15 29 | ### Added 30 | - `uploadVideo` option. Allows uploading Cypress videos for specs. Check the readme for details. 31 | 32 | ## [5.3.2] - 2024-07-01 33 | ### Fixed 34 | - Launch finishing for tests annotated with '.skip'. 35 | 36 | ## [5.3.1] - 2024-06-24 37 | ### Fixed 38 | - [#192](https://github.com/reportportal/agent-js-cypress/issues/192). Reporter procreates an enormous amount of processes during execution. Thanks to [epam-avramenko](https://github.com/epam-avramenko). 39 | ### Changed 40 | - `@reportportal/client-javascript` bumped to version `5.1.4`, new `launchUuidPrintOutput` types introduced: 'FILE', 'ENVIRONMENT'. 41 | 42 | ## [5.3.0] - 2024-05-07 43 | ### Added 44 | - `cucumberStepStart` and `cucumberStepEnd` commands for reporting `cypress-cucumber-preprocessor` scenario steps as nested steps in RP. 45 | ### Security 46 | - Updated versions of vulnerable packages (@reportportal/client-javascript, glob). 47 | ### Deprecated 48 | - Node.js 12 usage. This minor version is the latest that supports Node.js 12. 49 | 50 | ## [5.2.0] - 2024-03-21 51 | ### Fixed 52 | - Display stack trace for failed test cases 53 | ### Changed 54 | - **Breaking change** Drop support of Node.js 10. The version [5.1.5](https://github.com/reportportal/agent-js-cypress/releases/tag/v5.1.5) is the latest that supports it. 55 | - `@reportportal/client-javascript` bumped to version `5.1.0`. 56 | 57 | ## [5.1.5] - 2024-01-19 58 | ### Deprecated 59 | - Node.js 10 usage. This version is the latest that supports Node.js 10. 60 | 61 | ## [5.1.4] - 2023-10-23 62 | ### Fixed 63 | - `launchId` option is not recognized - resolves [#171](https://github.com/reportportal/agent-js-cypress/issues/171). 64 | ### Changed 65 | - `@reportportal/client-javascript` bumped to version `5.0.14`. `launchUuidPrint` and `launchUuidPrintOutput` configuration options introduced. 66 | 67 | ## [5.1.3] - 2023-07-18 68 | ### Added 69 | - TypeScript declarations for `reportPortalCommands`. Thanks to [thomaswinkler](https://github.com/thomaswinkler). 70 | ### Changed 71 | - `token` configuration option was renamed to `apiKey` to maintain common convention. 72 | - `@reportportal/client-javascript` bumped to version `5.0.12`. 73 | 74 | ## [5.1.2] - 2023-02-27 75 | ### Fixed 76 | - Screenshots are missing in some cases. The mechanism for attaching screenshots has been completely rewritten. Thanks to [thomaswinkler](https://github.com/thomaswinkler). 77 | - Unhandled promise rejections while sending logs. Thanks to [Nigui](https://github.com/Nigui). 78 | ### Security 79 | - Updated versions of vulnerable packages (minimatch, nanoid, jsdom, json5, node-notifier). 80 | 81 | ## [5.1.1] - 2023-01-24 82 | ### Added 83 | - `mergeOptions` parameter to `mergeLaunches`. 84 | ### Fixed 85 | - Pending Cypress tests are now marked as skipped in the ReportPortal and finishes correctly. Thanks to [thomaswinkler](https://github.com/thomaswinkler). 86 | - `mode` option proper handling. Thanks to [thomaswinkler](https://github.com/thomaswinkler). 87 | ### Updated 88 | - `@reportportal/client-javascript` bumped to version `5.0.8`. 89 | ### Security 90 | - Updated versions of vulnerable packages (qs, minimatch, decode-uri-component). 91 | 92 | ## [5.1.0] - 2022-09-22 93 | ### Added 94 | - Cypress 10.x versions support (closes [116](https://github.com/reportportal/agent-js-cypress/issues/116) and [115](https://github.com/reportportal/agent-js-cypress/issues/115)). Thanks to [orgads](https://github.com/orgads) and [dwentland24](https://github.com/dwentland24). 95 | - The Readme file and examples in [examples repository](https://github.com/reportportal/examples-js) have been updated accordingly. 96 | ### Security 97 | - Updated version of vulnerable `ansi-regex` package. 98 | 99 | ## [5.0.4] - 2022-07-12 100 | ### Fixed 101 | - 'Error: cannot read property toString of undefined' for _log_ command. 102 | - Vulnerabilities (minimist, follow-redirects). 103 | 104 | ## [5.0.3] - 2022-01-27 105 | ### Fixed 106 | - [#76](https://github.com/reportportal/agent-js-cypress/issues/76) Custom screenshot command doesn't wait for image to be taken. 107 | - [95](https://github.com/reportportal/agent-js-cypress/issues/95) and [97](https://github.com/reportportal/agent-js-cypress/issues/97) with 9.* cypress versions support. 108 | ### Changed 109 | - Package size reduced 110 | 111 | ## [5.0.2] - 2021-05-18 112 | ### Added 113 | - [#65](https://github.com/reportportal/agent-js-cypress/issues/65) Merge launches for parallel run. 114 | ### Fixed 115 | - Vulnerabilities (axios, acorn, ini, y18n, hosted-git-info). 116 | 117 | ## [5.0.1] - 2020-06-30 118 | ### Fixed 119 | - [#53](https://github.com/reportportal/agent-js-cypress/issues/53) Fix merge launches for `isLaunchMergeRequired` option. 120 | 121 | ## [5.0.0] - 2020-06-22 122 | ### Added 123 | - Full compatibility with ReportPortal version 5.* (see [reportportal releases](https://github.com/reportportal/reportportal/releases)) 124 | - Cypress plugin to extend the functionality of the reporter (see [ReportPortal custom commands](https://github.com/reportportal/agent-js-cypress#reportportal-custom-commands)) 125 | ### Deprecated 126 | - Previous package version [agent-js-cypress](https://www.npmjs.com/package/agent-js-cypress) will no longer supported by reportportal.io 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @reportportal/agent-js-cypress 2 | 3 | Agent to integrate Cypress with ReportPortal. 4 | * More about [Cypress](https://cypress.io/) 5 | * More about [ReportPortal](http://reportportal.io/) 6 | 7 | ## Install 8 | 9 | ```console 10 | $ npm install --save-dev @reportportal/agent-js-cypress 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Cypress version => 10 16 | 17 | There is a configuration guide for Cypress version 10 and above. 18 | 19 | #### cypress.config.js 20 | 21 | Add the following options to cypress.config.js. 22 | 23 | ```javascript 24 | 25 | const { defineConfig } = require('cypress'); 26 | const registerReportPortalPlugin = require('@reportportal/agent-js-cypress/lib/plugin'); 27 | 28 | module.exports = defineConfig({ 29 | reporter: '@reportportal/agent-js-cypress', 30 | reporterOptions: { 31 | apiKey: '', 32 | endpoint: 'https://your.reportportal.server/api/v1', 33 | project: 'Your reportportal project name', 34 | launch: 'Your launch name', 35 | description: 'Your launch description', 36 | attributes: [ 37 | { 38 | key: 'attributeKey', 39 | value: 'attributeValue', 40 | }, 41 | { 42 | value: 'anotherAttributeValue', 43 | }, 44 | ], 45 | }, 46 | e2e: { 47 | setupNodeEvents(on, config) { 48 | return registerReportPortalPlugin(on, config); 49 | }, 50 | }, 51 | }); 52 | ``` 53 | To see more options refer [Options](#options). 54 | 55 | #### Setup [ReportPortal custom commands](#reportportal-custom-commands) 56 | 57 | Add the following to your custom commands file (cypress/support/commands.js): 58 | 59 | ```javascript 60 | 61 | require('@reportportal/agent-js-cypress/lib/commands/reportPortalCommands'); 62 | ``` 63 | 64 | See examples of usage [here](https://github.com/reportportal/examples-js/tree/master/example-cypress). 65 | 66 | ### Cypress version <= 9 67 | 68 | There is a configuration guide for Cypress version 9 and below. 69 | 70 | #### Cypress.json 71 | 72 | Add the following options to cypress.json 73 | 74 | ```json 75 | 76 | { 77 | "reporter": "@reportportal/agent-js-cypress", 78 | "reporterOptions": { 79 | "apiKey": "", 80 | "endpoint": "https://your.reportportal.server/api/v1", 81 | "project": "Your reportportal project name", 82 | "launch": "Your launch name", 83 | "description": "Your launch description", 84 | "attributes": [ 85 | { 86 | "key": "attributeKey", 87 | "value": "attributeValue" 88 | }, 89 | { 90 | "value": "anotherAttributeValue" 91 | } 92 | ] 93 | } 94 | } 95 | 96 | ``` 97 | 98 | To see more options refer [Options](#options). 99 | 100 | #### Register ReportPortal plugin (cypress/plugins/index.js): 101 | 102 | ```javascript 103 | const registerReportPortalPlugin = require('@reportportal/agent-js-cypress/lib/plugin'); 104 | 105 | module.exports = (on, config) => registerReportPortalPlugin(on, config); 106 | ``` 107 | 108 | #### Setup [ReportPortal custom commands](#reportportal-custom-commands) 109 | 110 | Add the following to your custom commands file (cypress/support/commands.js): 111 | 112 | ```javascript 113 | require('@reportportal/agent-js-cypress/lib/commands/reportPortalCommands'); 114 | ``` 115 | 116 | ## Options 117 | 118 | The full list of available options presented below. 119 | 120 | | Option | Necessity | Default | Description | 121 | |-----------------------------|------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 122 | | apiKey | Required | | User's reportportal token from which you want to send requests. It can be found on the profile page of this user. | 123 | | endpoint | Required | | URL of your server. For example 'https://server:8080/api/v1'. | 124 | | launch | Required | | Name of launch at creation. | 125 | | project | Required | | The name of the project in which the launches will be created. | 126 | | attributes | Optional | [] | Launch attributes. | 127 | | description | Optional | '' | Launch description. | 128 | | rerun | Optional | false | Enable [rerun](https://reportportal.io/docs/dev-guides/RerunDevelopersGuide) | 129 | | rerunOf | Optional | Not set | UUID of launch you want to rerun. If not specified, reportportal will update the latest launch with the same name | 130 | | mode | Optional | 'DEFAULT' | Results will be submitted to Launches page
*'DEBUG'* - Results will be submitted to Debug page. | 131 | | skippedIssue | Optional | true | reportportal provides feature to mark skipped tests as not 'To Investigate'.
Option could be equal boolean values:
*true* - skipped tests considered as issues and will be marked as 'To Investigate' on reportportal.
*false* - skipped tests will not be marked as 'To Investigate' on application. | 132 | | debug | Optional | false | This flag allows seeing the logs of the client-javascript. Useful for debugging. | 133 | | launchId | Optional | Not set | The _ID_ of an already existing launch. The launch must be in 'IN_PROGRESS' status while the tests are running. Please note that if this _ID_ is provided, the launch will not be finished at the end of the run and must be finished separately. | 134 | | launchUuidPrint | Optional | false | Whether to print the current launch UUID. | 135 | | launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR'. Works only if `launchUuidPrint` set to `true`. | 136 | | restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. | 137 | | uploadVideo | Optional | false | Whether to upload the Cypress video. Uploads videos for failed specs only. To upload videos for specs with other statuses, set also the `uploadVideoForNonFailedSpec` to `true`. | 138 | | uploadVideoForNonFailedSpec | Optional | false | Whether to upload the Cypress video for a non-failed specs. Works only if `uploadVideo` set to `true`. | 139 | | waitForVideoTimeout | Optional | 10000 | Value in `ms`. Since Cypress video processing may take extra time after the spec is complete, there is a timeout to wait for the video file readiness. Works only if `uploadVideo` set to `true`. | 140 | | waitForVideoInterval | Optional | 500 | Value in `ms`. Interval to check if the video file is ready. The interval is used until `waitForVideoTimeout` is reached. Works only if `uploadVideo` set to `true`. | 141 | | videoCompression | Optional | false | Whether to compress the Cypress video by the agent before uploading it to the ReportPortal. Settings the same as for [Cypress video compression](https://docs.cypress.io/guides/references/configuration#Videos:~:text=cypress%20run.-,videoCompression,-false). The quality setting for the video compression, in Constant Rate Factor (CRF). The value can be `false` or `0` to disable compression or a CRF between `1` and `51`, where a lower value results in better quality (at the expense of a higher file size). Setting this option to `true` will result in a default CRF of 32. Works only if `uploadVideo` set to `true`. | 142 | | autoMerge | Optional | false | Enable automatic report test items of all run spec into one launch. You should install plugin or setup additional settings in reporterOptions. See [Automatically merge launch](#automatically-merge-launches). | 143 | | reportHooks | Optional | false | Determines report before and after hooks or not. | 144 | | isLaunchMergeRequired | Optional | false | Allows to merge Cypress run's into one launch at the end of the run. Needs additional setup. See [Manual merge launches](#manual-merge-launches). | 145 | | parallel | Optional | false | Indicates to the reporter that spec files will be executed in parallel on different machines. Parameter could be equal boolean values. See [Parallel execution](#parallel-execution). | 146 | | debugIpc | Optional | false | This flag allows seeing the debug logs of the internal node-ipc server and client. | 147 | | retryIpcInterval | Optional | 1500 | Value in `ms`. Interval for node-ipc client to retry connection to node-ipc server. Retry count is unlimited. | 148 | | token | Deprecated | Not set | Use `apiKey` instead. | 149 | 150 | ### Overwrite options from config file 151 | 152 | **If you run Cypress tests programmatically or use `cypress.config.js`, you can simply overwrite them:** 153 | 154 | ```javascript 155 | const updatedConfig = { 156 | ...config, 157 | reporterOptions: { 158 | ...config.reporterOptions, 159 | apiKey: process.env.RP_API_KEY, 160 | }, 161 | }; 162 | ``` 163 | 164 | **For security reasons, you can also set token as a part of Environment Variables, instead of sharing it in the config file:** 165 | 166 | | Option | ENV variable | Note | 167 | |-------------|-----------------|----------------------------------------| 168 | | apiKey | RP_API_KEY || 169 | | token | RP_TOKEN | *deprecated* Use `RP_API_KEY` instead. | 170 | 171 | ## ReportPortal custom commands 172 | 173 | ### Logging 174 | 175 | ReportPortal provides the following custom commands for reporting logs into the current test. 176 | 177 | * cy.log(*message*). Overrides standard Cypress `cy.log(log)`. Reports *message* as an info log of the current test.
178 | 179 | You can use the following methods to report logs and attachments with different log levels: 180 | * cy.trace (*message* , *file*). Reports *message* and optional *file* as a log of the current test with trace log level. 181 | * cy.logDebug (*message* , *file*). Reports *message* and optional *file* as a log of the current test with debug log level. 182 | * cy.info (*message* , *file*). Reports *message* and optional *file* as log of the current test with info log level. 183 | * cy.warn (*message* , *file*). Reports *message* and optional *file* as a log of the current test with warning log level. 184 | * cy.error (*message* , *file*). Reports *message* and optional *file* as a log of the current test with error log level. 185 | * cy.fatal (*message* , *file*). Reports *message* and optional *file* as a log of the current test with fatal log level. 186 | * cy.launchTrace (*message* , *file*). Reports *message* and optional *file* as a log of the launch with trace log level. 187 | * cy.launchDebug (*message* , *file*). Reports *message* and optional *file* as a log of the launch with debug log level. 188 | * cy.launchInfo (*message* , *file*). Reports *message* and optional *file* as log of the launch with info log level. 189 | * cy.launchWarn (*message* , *file*). Reports *message* and optional *file* as a log of the launch with warning log level. 190 | * cy.launchError (*message* , *file*). Reports *message* and optional *file* as a log of the launch with error log level. 191 | * cy.launchFatal (*message* , *file*). Reports *message* and optional *file* as a log of the launch with fatal log level. 192 | 193 | *file* should be an object:
194 | ```javascript 195 | { 196 | name: "filename", 197 | type: "image/png", // media type 198 | content: data, // file content represented as 64base string 199 | } 200 | ``` 201 | 202 | **Note:** The `cy.debug` RP command has been changed to `cy.logDebug` due to the command with the same name in Cypress 9.*. 203 | 204 | ### Report attributes for tests 205 | 206 | **addTestAttributes (*attributes*)**. Add attributes(tags) to the current test. Should be called inside of corresponding test.
207 | *attributes* is array of pairs of key and value: 208 | ```javascript 209 | [{ 210 | key: "attributeKey1", 211 | value: "attributeValue2", 212 | }] 213 | ``` 214 | *Key* is optional field. 215 | 216 | ### Integration with Sauce Labs 217 | 218 | To integrate with Sauce Labs just add attributes: 219 | 220 | ```javascript 221 | [{ 222 | "key": "SLID", 223 | "value": "# of the job in Sauce Labs" 224 | }, { 225 | "key": "SLDC", 226 | "value": "EU (EU or US)" 227 | }] 228 | ``` 229 | 230 | ### Report description for tests 231 | 232 | **setTestDescription (*description*)**. Set text description to the current test. Should be called inside of corresponding test. 233 | 234 | ### Report test case Id for tests and suites 235 | 236 | **setTestCaseId (*id*, *suite*)**. Set test case id to the current test or suite. Should be called inside of corresponding test/suite.
237 | *id* is a string test case Id.
238 | *suite (optional)* is the title of the suite to which the specified test case id belongs. Should be provided just in case of reporting test case id for specified suite instead of current test. 239 | 240 | ### Finish launch/test item with status 241 | 242 | ReportPortal provides the following custom commands for setting status to the current suite/spec. 243 | 244 | * cy.setStatus(*status*, *suite*). Assign *status* to the current test or suite. Should be called inside of corresponding test/suite.
245 | *status* should be equal to one of the following values: *passed*, *failed*, *stopped*, *skipped*, *interrupted*, *cancelled*, *info*, *warn*.
246 | *suite (optional)* is the title of the suite to which you wish to set the status (all suite descriptions must be unique). Should be provided just in case of assign status for specified suite instead of current test.
247 | 248 | You can use the shorthand forms of the cy.setStatus method: 249 | 250 | * cy.setStatusPassed(*suite*). Assign *passed* status to the current test or suite. 251 | * cy.setStatusFailed(*suite*). Assign *failed* status to the current test or suite. 252 | * cy.setStatusSkipped(*suite*). Assign *skipped* status to the current test or suite. 253 | * cy.setStatusStopped(*suite*). Assign *stopped* status to the current test or suite. 254 | * cy.setStatusInterrupted(*suite*). Assign *interrupted* status to the current test or suite. 255 | * cy.setStatusCancelled(*suite*). Assign *cancelled* status to the current test or suite. 256 | * cy.setStatusInfo(*suite*). Assign *info* status to the current test or suite. 257 | * cy.setStatusWarn(*suite*). Assign *warn* status to the current test or suite. 258 | 259 | ReportPortal also provides the corresponding methods for setting status into the launch: 260 | * setLaunchStatus(*status*). Assign *status* to the launch.
261 | *status* should be equal to one of the following values: *passed*, *failed*, *stopped*, *skipped*, *interrupted*, *cancelled*, *info*, *warn*.
262 | * cy.setLaunchStatusPassed(). Assign *passed* status to the launch. 263 | * cy.setLaunchStatusFailed(). Assign *failed* status to the launch. 264 | * cy.setLaunchStatusSkipped(). Assign *skipped* status to the launch. 265 | * cy.setLaunchStatusStopped(). Assign *stopped* status to the launch. 266 | * cy.setLaunchStatusInterrupted(). Assign *interrupted* status to the launch. 267 | * cy.setLaunchStatusCancelled(). Assign *cancelled* status to the launch. 268 | * cy.setLaunchStatusInfo(). Assign *info* status to the launch. 269 | 270 | ## Screenshots support 271 | 272 | To use custom filename in `cy.screenshot` function you should [setup ReportRortal custom commands](#setup-reportportal-custom-commands). 273 | Default usage of Cypress screenshot function is supported without additional setup. 274 | 275 | ## Report a single launch 276 | 277 | By default, Cypress create a separate run for each test file. 278 | This section describe how to report test items of different specs into the single launch. 279 | 280 | The agent supports the `launchId` parameter to specify the ID of the already started launch.
281 | This way, you can start the launch using `@reportportal/client-javascript` before the test run and then specify its ID in the config. 282 | 283 | With launch ID provided, the agent will attach all test results to that launch. So it won't be finished by the agent and should be finished separately. 284 | 285 | All necessary adjustments are performed in the `setupNodeEvents` function. 286 | 287 | ```javascript 288 | const { defineConfig } = require('cypress'); 289 | const registerReportPortalPlugin = require('@reportportal/agent-js-cypress/lib/plugin'); 290 | const rpClient = require('@reportportal/client-javascript'); 291 | 292 | const reportportalOptions = { 293 | autoMerge: false, // please note that `autoMerge` should be disabled 294 | //... 295 | }; 296 | 297 | export default defineConfig({ 298 | //... 299 | reporter: '@reportportal/agent-js-cypress', 300 | reporterOptions: reportportalOptions, 301 | e2e: { 302 | //... 303 | async setupNodeEvents(on, config) { 304 | const client = new rpClient(reportportalOptions); 305 | 306 | async function startLaunch() { 307 | // see https://github.com/reportportal/client-javascript?tab=readme-ov-file#startlaunch for the details 308 | const { tempId, promise } = client.startLaunch({ 309 | name: options.launch, 310 | attributes: options.attributes, 311 | // etc. 312 | }); 313 | const response = await promise; 314 | 315 | return { tempId, launchId: response.id }; 316 | } 317 | const { tempId, launchId } = await startLaunch(); 318 | 319 | on('after:run', async () => { 320 | const finishLaunch = async () => { 321 | // see https://github.com/reportportal/client-javascript?tab=readme-ov-file#finishlaunch for the details 322 | await client.finishLaunch(tempId, {}).promise; 323 | }; 324 | 325 | await finishLaunch(); 326 | }); 327 | 328 | registerReportPortalPlugin(on, config); 329 | 330 | // return the `launchId` from `setupNodeEvents` to allow Cypress merge it with the existing config (https://docs.cypress.io/api/node-events/overview#setupNodeEvents:~:text=If%20you%20return%20or%20resolve%20with%20an%20object%2C) 331 | return { 332 | reporterOptions: { 333 | launchId, 334 | }, 335 | }; 336 | }, 337 | //... 338 | }, 339 | }); 340 | ``` 341 | 342 | That's it, now all test results will be attached to the single launch. 343 | 344 | **Note:** This approach will likely be incorporated into the plugin in future versions of the agent. 345 | 346 | ## Automatically merge launches 347 | 348 | Unstable. See [Report a single launch](#report-a-single-launch) for the recommended approach. 349 | 350 | By default, Cypress create a separate run for each test file. This section describe how to report test items of different specs into the single launch. 351 | This feature needs information about Cypress configuration. To provide it to the reporter you need to install reportPortal plugin (see how to in [this section](#register-reportportal-plugin-cypresspluginsindexjs)). 352 | 353 | **Enable autoMerge in reporterOptions as shown below:** 354 | 355 | ```json 356 | 357 | { 358 | ... 359 | "reporterOptions": { 360 | ... 361 | "autoMerge": true 362 | } 363 | } 364 | ``` 365 | 366 | **Please note**, that `autoMerge` feature is unstable in some cases (e.g. when using `cypress-grep` or `--spec` CLI argument to specify the test amount that should be executed) and may lead to unfinished launches in ReportPortal. 367 | 368 | If this is the case, please specify `specPattern` in the config directly. You can also use the [Report a single launch](#manual-merge-launches) instead. 369 | 370 | ## Manual merge launches 371 | 372 | Deprecated. See [Report a single launch](#report-a-single-launch) for the actual approach. 373 | 374 | ## Parallel execution 375 | 376 | Cypress can run recorded tests in parallel across multiple machines since version 3.1.0 ([Cypress docs](https://docs.cypress.io/guides/guides/parallelization)).
377 | By default Cypress create a separate run for each test file. To merge all runs into one launch in Report Portal you need to provide [autoMerge](#automatically-merge-launches) option together with `parallel` flag.
378 | Since Cypress does not provide the ci_build_id to the reporter, you need to provide it manually using the `CI_BUILD_ID` environment variable (see [Cypress docs](https://docs.cypress.io/guides/guides/parallelization#CI-Build-ID-environment-variables-by-provider) for details). 379 | 380 | **Enable parallel in reporterOptions as shown below:** 381 | 382 | ```javascript 383 | 384 | { 385 | ... 386 | reporterOptions: { 387 | ... 388 | parallel: true 389 | } 390 | } 391 | 392 | ``` 393 | 394 | **Here's an example of setting up parallel Cypress execution on several machines using GitHub Actions:** 395 | 396 | ```yaml 397 | 398 | name: CI-pipeline 399 | 400 | on: 401 | pull_request: 402 | 403 | jobs: 404 | test: 405 | runs-on: ubuntu-latest 406 | container: cypress/browsers:node12.18.3-chrome87-ff82 407 | strategy: 408 | fail-fast: false 409 | matrix: 410 | containers: [1, 2, 3] 411 | env: 412 | CI_BUILD_ID: ${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }} 413 | steps: 414 | - name: Checkout 415 | uses: actions/checkout@v2 416 | 417 | - name: 'UI Tests - Chrome' 418 | uses: cypress-io/github-action@v2 419 | with: 420 | config-file: cypress.json 421 | group: 'UI Tests - Chrome' 422 | spec: cypress/integration/* 423 | record: true 424 | parallel: true 425 | env: 426 | CYPRESS_RECORD_KEY: ${{ secrets.RECORD_KEY }} 427 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 428 | ACTIONS_RUNNER_DEBUG: true 429 | 430 | ``` 431 | 432 | **Note:** The example provided for Cypress version <= 9. For Cypress version >= 10 usage of `cypress-io/github-action` may be changed. 433 | 434 | ## Cypress-cucumber-preprocessor execution 435 | 436 | ### Configuration: 437 | 438 | Specify the options in the cypress.config.js: 439 | 440 | ```javascript 441 | const { defineConfig } = require('cypress'); 442 | const createBundler = require('@bahmutov/cypress-esbuild-preprocessor'); 443 | const preprocessor = require('@badeball/cypress-cucumber-preprocessor'); 444 | const createEsbuildPlugin = require('@badeball/cypress-cucumber-preprocessor/esbuild').default; 445 | const registerReportPortalPlugin = require('@reportportal/agent-js-cypress/lib/plugin'); 446 | 447 | module.exports = defineConfig({ 448 | reporter: '@reportportal/agent-js-cypress', 449 | reporterOptions: { 450 | endpoint: 'http://your-instance.com:8080/api/v1', 451 | apiKey: 'reportportalApiKey', 452 | launch: 'LAUNCH_NAME', 453 | project: 'PROJECT_NAME', 454 | description: 'LAUNCH_DESCRIPTION', 455 | }, 456 | e2e: { 457 | async setupNodeEvents(on, config) { 458 | await preprocessor.addCucumberPreprocessorPlugin(on, config); 459 | on( 460 | 'file:preprocessor', 461 | createBundler({ 462 | plugins: [createEsbuildPlugin(config)], 463 | }), 464 | ); 465 | registerReportPortalPlugin(on, config); 466 | 467 | return config; 468 | }, 469 | specPattern: 'cypress/e2e/**/*.feature', 470 | supportFile: 'cypress/support/e2e.js', 471 | }, 472 | }); 473 | ``` 474 | 475 | ### Scenario steps 476 | 477 | At the moment it is not possible to subscribe to start and end of scenario steps events. To solve the problem with displaying steps in the ReportPortal, the agent provides special commands: `cucumberStepStart`, `cucumberStepEnd`. 478 | To work correctly, these commands must be called in the `BeforeStep`/`AfterStep` hooks. 479 | 480 | ```javascript 481 | import { BeforeStep, AfterStep } from '@badeball/cypress-cucumber-preprocessor'; 482 | 483 | BeforeStep((step) => { 484 | cy.cucumberStepStart(step); 485 | }); 486 | 487 | AfterStep((step) => { 488 | cy.cucumberStepEnd(step); 489 | }); 490 | ``` 491 | 492 | You can avoid duplicating this logic in each step definitions. Instead, add it to the `cypress/support/step_definitions.js` file and include the path to this file in the [stepDefinitions](https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/step-definitions.md) array (if necessary) within cucumber-preprocessor config. These hooks will be used for all step definitions. 493 | 494 | # Copyright Notice 495 | 496 | Licensed under the [Apache License v2.0](LICENSE) 497 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 5.5.1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/cypressReporter'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | moduleFileExtensions: ['js'], 19 | testRegex: '/test/.*\\.test.(js)$', 20 | collectCoverageFrom: [ 21 | 'lib/**/*.js', 22 | '!lib/commands/**/*.js', 23 | '!lib/plugin/**/*.js', 24 | '!lib/mergeLaunchesUtils.js', 25 | '!lib/mergeLaunches.js', 26 | '!lib/cypressReporter.js', 27 | '!lib/ipcEvents.js', 28 | '!lib/ipcServer.js', 29 | '!lib/testStatuses.js', 30 | '!lib/worker.js', 31 | '!lib/utils/attachments.js', 32 | ], 33 | coverageThreshold: { 34 | global: { 35 | branches: 80, 36 | functions: 80, 37 | lines: 80, 38 | statements: 80, 39 | }, 40 | }, 41 | testPathIgnorePatterns: [ 42 | '/cypress/', 43 | '/node_modules/', 44 | '/lib/test/mock/', 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /lib/commands/reportPortalCommands.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | type RP_ATTRIBUTES = { key?: string; value: string }; 5 | 6 | type RP_STATUS = 7 | | 'passed' 8 | | 'failed' 9 | | 'skipped' 10 | | 'stopped' 11 | | 'interrupted' 12 | | 'cancelled' 13 | | 'info' 14 | | 'warn'; 15 | 16 | type RP_FILE = { 17 | // file name 18 | name: string; 19 | // media type, such as image/png 20 | type: string; 21 | // base64 string 22 | content: string; 23 | }; 24 | 25 | namespace Cypress { 26 | interface Chainable { 27 | addTestAttributes(attributes: RP_ATTRIBUTES[]): Chainable; 28 | 29 | setTestDescription(description: string): Chainable; 30 | 31 | setTestCaseId(testCaseId: string, suiteTitle?: string): Chainable; 32 | 33 | trace(message: string, file?: RP_FILE): Chainable; 34 | 35 | logDebug(message: string, file?: RP_FILE): Chainable; 36 | 37 | info(message: string, file?: RP_FILE): Chainable; 38 | 39 | warn(message: string, file?: RP_FILE): Chainable; 40 | 41 | error(message: string, file?: RP_FILE): Chainable; 42 | 43 | fatal(message: string, file?: RP_FILE): Chainable; 44 | 45 | launchTrace(message: string, file?: RP_FILE): Chainable; 46 | 47 | launchDebug(message: string, file?: RP_FILE): Chainable; 48 | 49 | launchInfo(message: string, file?: RP_FILE): Chainable; 50 | 51 | launchWarn(message: string, file?: RP_FILE): Chainable; 52 | 53 | launchError(message: string, file?: RP_FILE): Chainable; 54 | 55 | launchFatal(message: string, file?: RP_FILE): Chainable; 56 | // Waiting for migrate to TypeScript 57 | // Expected step: IStepHookParameter (https://github.com/badeball/cypress-cucumber-preprocessor/blob/055d8df6a62009c94057b0d894a30e142cb87b94/lib/public-member-types.ts#L39) 58 | cucumberStepStart(step: any): Chainable; 59 | 60 | cucumberStepEnd(step: any): Chainable; 61 | 62 | setStatus(status: RP_STATUS, suiteTitle?: string): Chainable; 63 | 64 | setStatusPassed(suiteTitle?: string): Chainable; 65 | 66 | setStatusFailed(suiteTitle?: string): Chainable; 67 | 68 | setStatusSkipped(suiteTitle?: string): Chainable; 69 | 70 | setStatusStopped(suiteTitle?: string): Chainable; 71 | 72 | setStatusInterrupted(suiteTitle?: string): Chainable; 73 | 74 | setStatusCancelled(suiteTitle?: string): Chainable; 75 | 76 | setStatusInfo(suiteTitle?: string): Chainable; 77 | 78 | setStatusWarn(suiteTitle?: string): Chainable; 79 | 80 | setLaunchStatus(status: RP_STATUS): Chainable; 81 | 82 | setLaunchStatusPassed(): Chainable; 83 | 84 | setLaunchStatusFailed(): Chainable; 85 | 86 | setLaunchStatusSkipped(): Chainable; 87 | 88 | setLaunchStatusStopped(): Chainable; 89 | 90 | setLaunchStatusInterrupted(): Chainable; 91 | 92 | setLaunchStatusCancelled(): Chainable; 93 | 94 | setLaunchStatusInfo(): Chainable; 95 | 96 | setLaunchStatusWarn(): Chainable; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/commands/reportPortalCommands.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const { RP_STATUSES } = require('./../testStatuses'); 18 | 19 | /** 20 | * Log commands 21 | */ 22 | 23 | Cypress.Commands.overwrite('log', (originalFn, ...args) => { 24 | const message = args.reduce((result, logItem) => { 25 | if (typeof logItem === 'object') { 26 | return [result, JSON.stringify(logItem)].join(' '); 27 | } 28 | 29 | return [result, logItem ? logItem.toString() : ''].join(' '); 30 | }, ''); 31 | cy.task('rp_Log', { 32 | level: 'trace', 33 | message, 34 | }); 35 | originalFn(...args); 36 | }); 37 | 38 | Cypress.Commands.add('trace', (message, file) => { 39 | cy.task('rp_Log', { 40 | level: 'trace', 41 | message, 42 | file, 43 | }); 44 | }); 45 | 46 | Cypress.Commands.add('logDebug', (message, file) => { 47 | cy.task('rp_Log', { 48 | level: 'debug', 49 | message, 50 | file, 51 | }); 52 | }); 53 | 54 | Cypress.Commands.add('info', (message, file) => { 55 | cy.task('rp_Log', { 56 | level: 'info', 57 | message, 58 | file, 59 | }); 60 | }); 61 | 62 | Cypress.Commands.add('warn', (message, file) => { 63 | cy.task('rp_Log', { 64 | level: 'warn', 65 | message, 66 | file, 67 | }); 68 | }); 69 | 70 | Cypress.Commands.add('error', (message, file) => { 71 | cy.task('rp_Log', { 72 | level: 'error', 73 | message, 74 | file, 75 | }); 76 | }); 77 | 78 | Cypress.Commands.add('fatal', (message, file) => { 79 | cy.task('rp_Log', { 80 | level: 'fatal', 81 | message, 82 | file, 83 | }); 84 | }); 85 | 86 | Cypress.Commands.add('launchTrace', (message, file) => { 87 | cy.task('rp_launchLog', { 88 | level: 'trace', 89 | message, 90 | file, 91 | }); 92 | }); 93 | 94 | Cypress.Commands.add('launchDebug', (message, file) => { 95 | cy.task('rp_launchLog', { 96 | level: 'debug', 97 | message, 98 | file, 99 | }); 100 | }); 101 | 102 | Cypress.Commands.add('launchInfo', (message, file) => { 103 | cy.task('rp_launchLog', { 104 | level: 'info', 105 | message, 106 | file, 107 | }); 108 | }); 109 | 110 | Cypress.Commands.add('launchWarn', (message, file) => { 111 | cy.task('rp_launchLog', { 112 | level: 'warn', 113 | message, 114 | file, 115 | }); 116 | }); 117 | 118 | Cypress.Commands.add('launchError', (message, file) => { 119 | cy.task('rp_launchLog', { 120 | level: 'error', 121 | message, 122 | file, 123 | }); 124 | }); 125 | 126 | Cypress.Commands.add('launchFatal', (message, file) => { 127 | cy.task('rp_launchLog', { 128 | level: 'fatal', 129 | message, 130 | file, 131 | }); 132 | }); 133 | 134 | /** 135 | * Cucumber Scenario's steps commands 136 | */ 137 | Cypress.Commands.add('cucumberStepStart', (step) => { 138 | cy.task('rp_cucumberStepStart', step); 139 | }); 140 | 141 | Cypress.Commands.add('cucumberStepEnd', (step) => { 142 | cy.task('rp_cucumberStepEnd', step); 143 | }); 144 | 145 | /** 146 | * Attributes command 147 | */ 148 | Cypress.Commands.add('addTestAttributes', (attributes) => { 149 | cy.task('rp_addTestAttributes', { 150 | attributes, 151 | }); 152 | }); 153 | 154 | /** 155 | * Set test description command 156 | */ 157 | Cypress.Commands.add('setTestDescription', (description) => { 158 | cy.task('rp_setTestDescription', { 159 | description, 160 | }); 161 | }); 162 | 163 | /** 164 | * Set test case ID command 165 | */ 166 | Cypress.Commands.add('setTestCaseId', (testCaseId, suiteTitle) => { 167 | cy.task('rp_setTestCaseId', { 168 | testCaseId, 169 | suiteTitle, 170 | }); 171 | }); 172 | 173 | /** 174 | * Set test status commands 175 | */ 176 | Cypress.Commands.add('setStatus', (status, suiteTitle) => { 177 | cy.task('rp_setStatus', { 178 | status, 179 | suiteTitle, 180 | }); 181 | }); 182 | 183 | Cypress.Commands.add('setStatusPassed', (suiteTitle) => { 184 | cy.task('rp_setStatus', { 185 | status: RP_STATUSES.PASSED, 186 | suiteTitle, 187 | }); 188 | }); 189 | 190 | Cypress.Commands.add('setStatusFailed', (suiteTitle) => { 191 | cy.task('rp_setStatus', { 192 | status: RP_STATUSES.FAILED, 193 | suiteTitle, 194 | }); 195 | }); 196 | 197 | Cypress.Commands.add('setStatusSkipped', (suiteTitle) => { 198 | cy.task('rp_setStatus', { 199 | status: RP_STATUSES.SKIPPED, 200 | suiteTitle, 201 | }); 202 | }); 203 | 204 | Cypress.Commands.add('setStatusStopped', (suiteTitle) => { 205 | cy.task('rp_setStatus', { 206 | status: RP_STATUSES.STOPPED, 207 | suiteTitle, 208 | }); 209 | }); 210 | 211 | Cypress.Commands.add('setStatusInterrupted', (suiteTitle) => { 212 | cy.task('rp_setStatus', { 213 | status: RP_STATUSES.INTERRUPTED, 214 | suiteTitle, 215 | }); 216 | }); 217 | 218 | Cypress.Commands.add('setStatusCancelled', (suiteTitle) => { 219 | cy.task('rp_setStatus', { 220 | status: RP_STATUSES.CANCELLED, 221 | suiteTitle, 222 | }); 223 | }); 224 | 225 | Cypress.Commands.add('setStatusInfo', (suiteTitle) => { 226 | cy.task('rp_setStatus', { 227 | status: RP_STATUSES.INFO, 228 | suiteTitle, 229 | }); 230 | }); 231 | 232 | Cypress.Commands.add('setStatusWarn', (suiteTitle) => { 233 | cy.task('rp_setStatus', { 234 | status: RP_STATUSES.WARN, 235 | suiteTitle, 236 | }); 237 | }); 238 | 239 | /** 240 | * Set launch status commands 241 | */ 242 | Cypress.Commands.add('setLaunchStatus', (status) => { 243 | cy.task('rp_setLaunchStatus', { 244 | status, 245 | }); 246 | }); 247 | 248 | Cypress.Commands.add('setLaunchStatusPassed', () => { 249 | cy.task('rp_setLaunchStatus', { 250 | status: RP_STATUSES.PASSED, 251 | }); 252 | }); 253 | 254 | Cypress.Commands.add('setLaunchStatusFailed', () => { 255 | cy.task('rp_setLaunchStatus', { 256 | status: RP_STATUSES.FAILED, 257 | }); 258 | }); 259 | 260 | Cypress.Commands.add('setLaunchStatusSkipped', () => { 261 | cy.task('rp_setLaunchStatus', { 262 | status: RP_STATUSES.SKIPPED, 263 | }); 264 | }); 265 | 266 | Cypress.Commands.add('setLaunchStatusStopped', () => { 267 | cy.task('rp_setLaunchStatus', { 268 | status: RP_STATUSES.STOPPED, 269 | }); 270 | }); 271 | 272 | Cypress.Commands.add('setLaunchStatusInterrupted', () => { 273 | cy.task('rp_setLaunchStatus', { 274 | status: RP_STATUSES.INTERRUPTED, 275 | }); 276 | }); 277 | 278 | Cypress.Commands.add('setLaunchStatusCancelled', () => { 279 | cy.task('rp_setLaunchStatus', { 280 | status: RP_STATUSES.CANCELLED, 281 | }); 282 | }); 283 | 284 | Cypress.Commands.add('setLaunchStatusInfo', () => { 285 | cy.task('rp_setLaunchStatus', { 286 | status: RP_STATUSES.INFO, 287 | }); 288 | }); 289 | 290 | Cypress.Commands.add('setLaunchStatusWarn', () => { 291 | cy.task('rp_setLaunchStatus', { 292 | status: RP_STATUSES.WARN, 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const testItemStatuses = { 18 | PASSED: 'passed', 19 | FAILED: 'failed', 20 | SKIPPED: 'skipped', 21 | }; 22 | const logLevels = { 23 | ERROR: 'error', 24 | TRACE: 'trace', 25 | DEBUG: 'debug', 26 | INFO: 'info', 27 | WARN: 'warn', 28 | }; 29 | const entityType = { 30 | SUITE: 'suite', 31 | STEP: 'step', 32 | BEFORE_METHOD: 'BEFORE_METHOD', 33 | BEFORE_SUITE: 'BEFORE_SUITE', 34 | AFTER_METHOD: 'AFTER_METHOD', 35 | AFTER_SUITE: 'AFTER_SUITE', 36 | }; 37 | 38 | const hookTypes = { 39 | BEFORE_ALL: 'before all', 40 | BEFORE_EACH: 'before each', 41 | AFTER_ALL: 'after all', 42 | AFTER_EACH: 'after each', 43 | }; 44 | 45 | const hookTypesMap = { 46 | [hookTypes.BEFORE_EACH]: entityType.BEFORE_METHOD, 47 | [hookTypes.BEFORE_ALL]: entityType.BEFORE_SUITE, 48 | [hookTypes.AFTER_EACH]: entityType.AFTER_METHOD, 49 | [hookTypes.AFTER_ALL]: entityType.AFTER_SUITE, 50 | }; 51 | 52 | const reporterEvents = { 53 | INIT: 'rpInit', 54 | FULL_CONFIG: 'rpFullConfig', 55 | LOG: 'rpLog', 56 | LAUNCH_LOG: 'rpLaunchLog', 57 | ADD_ATTRIBUTES: 'rpAddAttrbiutes', 58 | SET_DESCRIPTION: 'rpSetDescription', 59 | SET_TEST_CASE_ID: 'setTestCaseId', 60 | SCREENSHOT: 'screenshot', 61 | SET_STATUS: 'setStatus', 62 | SET_LAUNCH_STATUS: 'setLaunchStatus', 63 | CUCUMBER_STEP_START: 'cucumberStepStart', 64 | CUCUMBER_STEP_END: 'cucumberStepEnd', 65 | }; 66 | 67 | const cucumberKeywordMap = { 68 | Outcome: 'Then', 69 | Action: 'When', 70 | Context: 'Given', 71 | }; 72 | 73 | const DEFAULT_IPC_RETRY_INTERVAL = 1500; 74 | 75 | module.exports = { 76 | testItemStatuses, 77 | logLevels, 78 | entityType, 79 | hookTypesMap, 80 | cucumberKeywordMap, 81 | reporterEvents, 82 | DEFAULT_IPC_RETRY_INTERVAL, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/cypressReporter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const Mocha = require('mocha'); 18 | 19 | const { 20 | EVENT_RUN_BEGIN, 21 | EVENT_RUN_END, 22 | EVENT_TEST_BEGIN, 23 | EVENT_TEST_END, 24 | EVENT_SUITE_BEGIN, 25 | EVENT_SUITE_END, 26 | EVENT_HOOK_BEGIN, 27 | EVENT_HOOK_END, 28 | EVENT_TEST_FAIL, 29 | EVENT_TEST_PENDING, 30 | } = Mocha.Runner.constants; 31 | const { fork } = require('child_process'); 32 | const { startIPCServer } = require('./ipcServer'); 33 | const { reporterEvents, testItemStatuses } = require('./constants'); 34 | const { IPC_EVENTS } = require('./ipcEvents'); 35 | const { 36 | getConfig, 37 | getLaunchStartObject, 38 | getSuiteStartInfo, 39 | getSuiteEndInfo, 40 | getTestInfo, 41 | getHookInfo, 42 | getTotalSpecs, 43 | } = require('./utils'); 44 | 45 | const { FAILED } = testItemStatuses; 46 | 47 | class CypressReporter extends Mocha.reporters.Base { 48 | constructor(runner, initialConfig) { 49 | super(runner); 50 | this.runner = runner; 51 | const config = getConfig(initialConfig); 52 | CypressReporter.currentLaunch += 1; 53 | CypressReporter.reporterOptions = config.reporterOptions; 54 | 55 | if ( 56 | CypressReporter.isFirstRun() || 57 | !config.reporterOptions.autoMerge || 58 | CypressReporter.reporterOptions.parallel 59 | ) { 60 | this.worker = fork(`${__dirname}/worker.js`, [], { 61 | detached: true, 62 | }); 63 | this.worker.send({ event: reporterEvents.INIT, config }); 64 | 65 | const configListener = (cypressFullConfig) => { 66 | this.worker.send({ event: reporterEvents.FULL_CONFIG, config: cypressFullConfig }); 67 | CypressReporter.cypressConfig = cypressFullConfig; 68 | CypressReporter.calcTotalLaunches(); 69 | }; 70 | const logListener = (log) => this.worker.send({ event: reporterEvents.LOG, log }); 71 | const launchLogListener = (log) => 72 | this.worker.send({ event: reporterEvents.LAUNCH_LOG, log }); 73 | const attributesListener = ({ attributes }) => 74 | this.worker.send({ event: reporterEvents.ADD_ATTRIBUTES, attributes }); 75 | const descriptionListener = ({ description }) => 76 | this.worker.send({ event: reporterEvents.SET_DESCRIPTION, description }); 77 | const testCaseId = (testCaseIdInfo) => 78 | this.worker.send({ event: reporterEvents.SET_TEST_CASE_ID, testCaseIdInfo }); 79 | const screenshotListener = (details) => 80 | this.worker.send({ event: reporterEvents.SCREENSHOT, details }); 81 | const setStatusListener = (statusInfo) => 82 | this.worker.send({ event: reporterEvents.SET_STATUS, statusInfo }); 83 | const setLaunchStatusListener = (statusInfo) => 84 | this.worker.send({ event: reporterEvents.SET_LAUNCH_STATUS, statusInfo }); 85 | const cucumberStepStartListener = (step) => 86 | this.worker.send({ event: reporterEvents.CUCUMBER_STEP_START, step }); 87 | const cucumberStepEndListener = (step) => 88 | this.worker.send({ event: reporterEvents.CUCUMBER_STEP_END, step }); 89 | 90 | startIPCServer( 91 | (server) => { 92 | server.on(IPC_EVENTS.CONFIG, configListener); 93 | server.on(IPC_EVENTS.LOG, logListener); 94 | server.on(IPC_EVENTS.LAUNCH_LOG, launchLogListener); 95 | server.on(IPC_EVENTS.ADD_ATTRIBUTES, attributesListener); 96 | server.on(IPC_EVENTS.SET_DESCRIPTION, descriptionListener); 97 | server.on(IPC_EVENTS.SET_TEST_CASE_ID, testCaseId); 98 | server.on(IPC_EVENTS.SCREENSHOT, screenshotListener); 99 | server.on(IPC_EVENTS.SET_STATUS, setStatusListener); 100 | server.on(IPC_EVENTS.SET_LAUNCH_STATUS, setLaunchStatusListener); 101 | server.on(IPC_EVENTS.CUCUMBER_STEP_START, cucumberStepStartListener); 102 | server.on(IPC_EVENTS.CUCUMBER_STEP_END, cucumberStepEndListener); 103 | }, 104 | (server) => { 105 | server.off(IPC_EVENTS.CONFIG, '*'); 106 | server.off(IPC_EVENTS.LOG, '*'); 107 | server.off(IPC_EVENTS.LAUNCH_LOG, '*'); 108 | server.off(IPC_EVENTS.ADD_ATTRIBUTES, '*'); 109 | server.off(IPC_EVENTS.SET_DESCRIPTION, '*'); 110 | server.off(IPC_EVENTS.SET_TEST_CASE_ID, '*'); 111 | server.off(IPC_EVENTS.SCREENSHOT, '*'); 112 | server.off(IPC_EVENTS.SET_STATUS, '*'); 113 | server.off(IPC_EVENTS.SET_LAUNCH_STATUS, '*'); 114 | server.off(IPC_EVENTS.CUCUMBER_STEP_START, '*'); 115 | server.off(IPC_EVENTS.CUCUMBER_STEP_END, '*'); 116 | }, 117 | { 118 | debugIpc: config.reporterOptions.debugIpc, 119 | retryIpcInterval: config.reporterOptions.retryIpcInterval, 120 | }, 121 | ); 122 | CypressReporter.worker = this.worker; 123 | } else { 124 | this.worker = CypressReporter.worker; 125 | } 126 | 127 | this.runner.on(EVENT_RUN_BEGIN, () => { 128 | if (CypressReporter.shouldStartLaunch()) { 129 | this.worker.send({ 130 | event: EVENT_RUN_BEGIN, 131 | launch: getLaunchStartObject(config), 132 | }); 133 | } 134 | }); 135 | 136 | this.runner.on(EVENT_SUITE_BEGIN, (suite) => { 137 | if (!suite.title) return; 138 | this.worker.send({ 139 | event: EVENT_SUITE_BEGIN, 140 | suite: getSuiteStartInfo(suite, this.runner.suite.file), 141 | }); 142 | }); 143 | 144 | this.runner.on(EVENT_SUITE_END, (suite) => { 145 | if (!suite.title) return; 146 | this.worker.send({ event: EVENT_SUITE_END, suite: getSuiteEndInfo(suite) }); 147 | }); 148 | 149 | this.runner.on(EVENT_TEST_BEGIN, (test) => { 150 | this.worker.send({ 151 | event: EVENT_TEST_BEGIN, 152 | test: getTestInfo(test, this.runner.suite.file), 153 | }); 154 | }); 155 | 156 | this.runner.on(EVENT_TEST_END, (test) => { 157 | this.worker.send({ event: EVENT_TEST_END, test: getTestInfo(test, this.runner.suite.file) }); 158 | }); 159 | 160 | this.runner.on(EVENT_TEST_PENDING, (test) => { 161 | this.worker.send({ 162 | event: EVENT_TEST_PENDING, 163 | test: getTestInfo(test, this.runner.suite.file), 164 | }); 165 | }); 166 | 167 | this.runner.on(EVENT_RUN_END, () => { 168 | CypressReporter.calcTotalLaunches(); 169 | if (CypressReporter.shouldStopLaunch()) { 170 | this.worker.send({ event: EVENT_RUN_END, launch: getLaunchStartObject(config) }); 171 | } 172 | }); 173 | 174 | this.runner.on(EVENT_HOOK_BEGIN, (hook) => { 175 | if (!config.reporterOptions.reportHooks) return; 176 | this.worker.send({ 177 | event: EVENT_HOOK_BEGIN, 178 | hook: getHookInfo(hook, this.runner.suite.file), 179 | }); 180 | }); 181 | 182 | this.runner.on(EVENT_HOOK_END, (hook) => { 183 | if (!config.reporterOptions.reportHooks) return; 184 | this.worker.send({ event: EVENT_HOOK_END, hook: getHookInfo(hook, this.runner.suite.file) }); 185 | }); 186 | 187 | this.runner.on(EVENT_TEST_FAIL, (test, err) => { 188 | if (test.failedFromHookId && config.reporterOptions.reportHooks) { 189 | this.worker.send({ 190 | event: EVENT_HOOK_END, 191 | hook: getHookInfo(test, this.runner.suite.file, FAILED, err), 192 | }); 193 | this.worker.send({ 194 | event: EVENT_TEST_END, 195 | test: getTestInfo(test, this.runner.suite.file, FAILED, err), 196 | }); 197 | } 198 | }); 199 | } 200 | 201 | static calcTotalLaunches() { 202 | if ( 203 | !CypressReporter.reporterOptions.autoMerge || 204 | CypressReporter.totalLaunches || 205 | CypressReporter.reporterOptions.parallel 206 | ) { 207 | return; 208 | } 209 | if (CypressReporter.cypressConfig) { 210 | CypressReporter.totalLaunches = getTotalSpecs(CypressReporter.cypressConfig); 211 | } else { 212 | console.log( 213 | 'Auto merge: plugin is not installed. Use reporterOptions settings for calculation.', 214 | ); 215 | CypressReporter.totalLaunches = getTotalSpecs(CypressReporter.reporterOptions); 216 | } 217 | } 218 | 219 | static shouldStopLaunch() { 220 | return ( 221 | !CypressReporter.totalLaunches || 222 | CypressReporter.currentLaunch === CypressReporter.totalLaunches || 223 | !CypressReporter.reporterOptions.autoMerge || 224 | CypressReporter.reporterOptions.parallel 225 | ); 226 | } 227 | 228 | static shouldStartLaunch() { 229 | return ( 230 | CypressReporter.isFirstRun() || 231 | !CypressReporter.totalLaunches || 232 | !CypressReporter.reporterOptions.autoMerge || 233 | CypressReporter.reporterOptions.parallel 234 | ); 235 | } 236 | 237 | static isFirstRun() { 238 | return CypressReporter.currentLaunch === 1; 239 | } 240 | } 241 | 242 | CypressReporter.currentLaunch = 0; 243 | CypressReporter.totalLaunches = 0; 244 | 245 | module.exports = CypressReporter; 246 | -------------------------------------------------------------------------------- /lib/ipcEvents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const IPC_EVENTS = { 18 | LOG: 'log', 19 | LAUNCH_LOG: 'launchLog', 20 | ADD_ATTRIBUTES: 'addAttributes', 21 | SET_DESCRIPTION: 'setDescription', 22 | SET_TEST_CASE_ID: 'setTestCaseId', 23 | CONFIG: 'config', 24 | SCREENSHOT: 'screenshot', 25 | SET_STATUS: 'setStatus', 26 | SET_LAUNCH_STATUS: 'setLaunchStatus', 27 | CUCUMBER_STEP_START: 'cucumberStepStart', 28 | CUCUMBER_STEP_END: 'cucumberStepEnd', 29 | }; 30 | 31 | module.exports = { IPC_EVENTS }; 32 | -------------------------------------------------------------------------------- /lib/ipcServer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * IPC server is used to communicate between ReportPortal plugin for Cypress and ReportPortal reporter 19 | * Plugin inits IPC client that send events to the reporter when processing custom ReportPortal commands 20 | * Reporter creates IPC server that receives events from the plugin. 21 | */ 22 | const ipc = require('node-ipc'); 23 | const { DEFAULT_IPC_RETRY_INTERVAL } = require('./constants'); 24 | 25 | const startIPCServer = ( 26 | subscribeServerEvents, 27 | unsubscribeServerEvents, 28 | { debugIpc, retryIpcInterval } = { 29 | debugIpc: false, 30 | retryIpcInterval: DEFAULT_IPC_RETRY_INTERVAL, 31 | }, 32 | ) => { 33 | if (ipc.server) { 34 | unsubscribeServerEvents(ipc.server); 35 | subscribeServerEvents(ipc.server); 36 | return; 37 | } 38 | ipc.config.id = 'reportportal'; 39 | ipc.config.retry = retryIpcInterval || DEFAULT_IPC_RETRY_INTERVAL; 40 | 41 | if (!debugIpc) { 42 | ipc.config.silent = true; 43 | } 44 | 45 | ipc.serve(() => { 46 | ipc.server.on('socket.disconnected', (socket, destroyedSocketID) => { 47 | ipc.log(`client ${destroyedSocketID} has disconnected!`); 48 | }); 49 | ipc.server.on('destroy', () => { 50 | ipc.log('server destroyed'); 51 | }); 52 | subscribeServerEvents(ipc.server); 53 | process.on('exit', () => { 54 | unsubscribeServerEvents(ipc.server); 55 | ipc.server.stop(); 56 | }); 57 | }); 58 | ipc.server.start(); 59 | }; 60 | 61 | module.exports = { startIPCServer }; 62 | -------------------------------------------------------------------------------- /lib/mergeLaunches.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const MAX_MERGE_TIMEOUT = 3600000; // 1 hour 18 | const CHECK_IN_PROGRESS_INTERVAL = 3000; 19 | 20 | const mergeLaunchesUtils = require('./mergeLaunchesUtils'); 21 | 22 | const mergeLaunches = (reporterOptions, mergeOptions = { extendSuitesDescription: false }) => { 23 | if (!mergeLaunchesUtils.isLaunchesInProgress(reporterOptions.launch)) { 24 | return mergeLaunchesUtils.callClientMergeLaunches(reporterOptions, mergeOptions); 25 | } 26 | const beginMergeTime = Date.now(); 27 | return new Promise((resolve, reject) => { 28 | const checkInterval = setInterval(() => { 29 | if (!mergeLaunchesUtils.isLaunchesInProgress(reporterOptions.launch)) { 30 | clearInterval(checkInterval); 31 | mergeLaunchesUtils 32 | .callClientMergeLaunches(reporterOptions, mergeOptions) 33 | .then(() => resolve()); 34 | } else if (Date.now() - beginMergeTime > MAX_MERGE_TIMEOUT) { 35 | clearInterval(checkInterval); 36 | reject(new Error(`Merge launch error. Timeout of ${MAX_MERGE_TIMEOUT}ms exceeded.`)); 37 | } 38 | }, CHECK_IN_PROGRESS_INTERVAL); 39 | }); 40 | }; 41 | 42 | const mergeParallelLaunches = async (client, config) => { 43 | const ciBuildId = process.env.CI_BUILD_ID; 44 | if (!ciBuildId) { 45 | console.error('For merge parallel launches CI_BUILD_ID must not be empty'); 46 | return; 47 | } 48 | try { 49 | // 1. Send request to get all launches with the same CI_BUILD_ID attribute value 50 | const params = new URLSearchParams({ 51 | 'filter.has.attributeValue': ciBuildId, 52 | }); 53 | const launchSearchUrl = `launch?${params.toString()}`; 54 | const response = await client.restClient.retrieveSyncAPI(launchSearchUrl, { 55 | headers: client.headers, 56 | }); 57 | // 2. Filter them to find launches that are in progress 58 | const launchesInProgress = response.content.filter((launch) => launch.status === 'IN_PROGRESS'); 59 | // 3. If exists, just return 60 | if (launchesInProgress.length) { 61 | return; 62 | } 63 | // 4. If not, merge all found launches with the same CI_BUILD_ID attribute value 64 | const launchIds = response.content.map((launch) => launch.id); 65 | const request = client.getMergeLaunchesRequest(launchIds); 66 | request.description = config.description; 67 | request.extendSuitesDescription = false; 68 | const mergeURL = 'launch/merge'; 69 | await client.restClient.create(mergeURL, request, { headers: client.headers }); 70 | } catch (err) { 71 | console.error('Fail to merge launches', err); 72 | } 73 | }; 74 | 75 | module.exports = { 76 | mergeLaunches, 77 | mergeParallelLaunches, 78 | }; 79 | -------------------------------------------------------------------------------- /lib/mergeLaunchesUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const glob = require('glob'); 19 | const RPClient = require('@reportportal/client-javascript'); 20 | 21 | const LAUNCH_LOCK_FILE_PREFIX = 'rplaunchinprogress'; 22 | 23 | const getLaunchLockFileName = (launchName, tempId) => 24 | `${LAUNCH_LOCK_FILE_PREFIX}-${launchName}-${tempId}.tmp`; 25 | 26 | const createMergeLaunchLockFile = (launchName, tempId) => { 27 | const filename = getLaunchLockFileName(launchName, tempId); 28 | fs.open(filename, 'w', (err) => { 29 | if (err) { 30 | throw err; 31 | } 32 | }); 33 | }; 34 | 35 | const deleteMergeLaunchLockFile = (launchName, tempId) => { 36 | const filename = getLaunchLockFileName(launchName, tempId); 37 | fs.unlink(filename, (err) => { 38 | if (err) { 39 | throw err; 40 | } 41 | }); 42 | }; 43 | 44 | const isLaunchesInProgress = (launchName) => { 45 | const files = glob.sync(`${LAUNCH_LOCK_FILE_PREFIX}-${launchName}-*.tmp`); 46 | return !!files.length; 47 | }; 48 | 49 | const callClientMergeLaunches = (reporterOptions, mergeOptions) => { 50 | const client = new RPClient(reporterOptions); 51 | return client.mergeLaunches(mergeOptions); 52 | }; 53 | 54 | module.exports = { 55 | getLaunchLockFileName, 56 | createMergeLaunchLockFile, 57 | deleteMergeLaunchLockFile, 58 | isLaunchesInProgress, 59 | callClientMergeLaunches, 60 | }; 61 | -------------------------------------------------------------------------------- /lib/plugin/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const ipc = require('node-ipc'); 18 | const { connectIPCClient } = require('./ipcClient'); 19 | const { IPC_EVENTS } = require('./../ipcEvents'); 20 | 21 | const registerReportPortalPlugin = (on, config, callbacks) => { 22 | connectIPCClient(config); 23 | 24 | on('task', { 25 | rp_Log(log) { 26 | ipc.of.reportportal.emit(IPC_EVENTS.LOG, log); 27 | return null; 28 | }, 29 | rp_launchLog(log) { 30 | ipc.of.reportportal.emit(IPC_EVENTS.LAUNCH_LOG, log); 31 | return null; 32 | }, 33 | rp_addTestAttributes(attributes) { 34 | ipc.of.reportportal.emit(IPC_EVENTS.ADD_ATTRIBUTES, attributes); 35 | return null; 36 | }, 37 | rp_setTestDescription(description) { 38 | ipc.of.reportportal.emit(IPC_EVENTS.SET_DESCRIPTION, description); 39 | return null; 40 | }, 41 | rp_setTestCaseId(testCaseIdInfo) { 42 | ipc.of.reportportal.emit(IPC_EVENTS.SET_TEST_CASE_ID, testCaseIdInfo); 43 | return null; 44 | }, 45 | rp_setStatus(statusInfo) { 46 | ipc.of.reportportal.emit(IPC_EVENTS.SET_STATUS, statusInfo); 47 | return null; 48 | }, 49 | rp_setLaunchStatus(statusInfo) { 50 | ipc.of.reportportal.emit(IPC_EVENTS.SET_LAUNCH_STATUS, statusInfo); 51 | return null; 52 | }, 53 | rp_cucumberStepStart(step) { 54 | ipc.of.reportportal.emit(IPC_EVENTS.CUCUMBER_STEP_START, step); 55 | return null; 56 | }, 57 | rp_cucumberStepEnd(step) { 58 | ipc.of.reportportal.emit(IPC_EVENTS.CUCUMBER_STEP_END, step); 59 | return null; 60 | }, 61 | }); 62 | 63 | on('after:screenshot', (screenshotInfo) => { 64 | let logMessage; 65 | if (callbacks && callbacks.screenshotLogFn && typeof callbacks.screenshotLogFn === 'function') { 66 | logMessage = callbacks.screenshotLogFn(screenshotInfo); 67 | } 68 | ipc.of.reportportal.emit(IPC_EVENTS.SCREENSHOT, { 69 | logMessage, 70 | screenshotInfo, 71 | }); 72 | return null; 73 | }); 74 | }; 75 | 76 | module.exports = registerReportPortalPlugin; 77 | -------------------------------------------------------------------------------- /lib/plugin/ipcClient.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * IPC server is used to communicate between ReportPortal plugin for Cypress and ReportPortal reporter 19 | * Plugin inits IPC client that send events to the reporter when processing custom ReportPortal commands 20 | * Reporter creates IPC server that recived info from the plugin. 21 | */ 22 | 23 | const ipc = require('node-ipc'); 24 | const { IPC_EVENTS } = require('./../ipcEvents'); 25 | const { DEFAULT_IPC_RETRY_INTERVAL } = require('../constants'); 26 | 27 | const connectIPCClient = (config) => { 28 | const retryInterval = 29 | config.reporterOptions.retryIpcInterval || 30 | // additional check if 'cypress-multi-reporters' is used 31 | config.reporterOptions.reportportalAgentJsCypressReporterOptions?.retryIpcInterval || 32 | DEFAULT_IPC_RETRY_INTERVAL; 33 | 34 | ipc.config.id = 'reportPortalReporter'; 35 | ipc.config.retry = retryInterval; 36 | 37 | if ( 38 | !config.reporterOptions.debugIpc && 39 | // additional check if 'cypress-multi-reporters' is used 40 | !config.reporterOptions.reportportalAgentJsCypressReporterOptions?.debugIpc 41 | ) { 42 | ipc.config.silent = true; 43 | } 44 | 45 | ipc.connectTo('reportportal', () => { 46 | ipc.of.reportportal.on('connect', () => { 47 | ipc.log('***connected to reportportal***'); 48 | ipc.of.reportportal.emit(IPC_EVENTS.CONFIG, config); 49 | }); 50 | ipc.of.reportportal.on('disconnect', () => { 51 | ipc.log('disconnected from reportportal'); 52 | }); 53 | }); 54 | }; 55 | 56 | module.exports = { connectIPCClient }; 57 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const RPClient = require('@reportportal/client-javascript'); 18 | const clientHelpers = require('@reportportal/client-javascript/lib/helpers'); 19 | 20 | const { entityType, logLevels, testItemStatuses, cucumberKeywordMap } = require('./constants'); 21 | const { 22 | getScreenshotAttachment, 23 | getTestStartObject, 24 | getTestEndObject, 25 | getHookStartObject, 26 | getAgentInfo, 27 | getCodeRef, 28 | getVideoFile, 29 | getSuiteStartObject, 30 | getSuiteEndObject, 31 | } = require('./utils'); 32 | 33 | const { createMergeLaunchLockFile, deleteMergeLaunchLockFile } = require('./mergeLaunchesUtils'); 34 | const { mergeParallelLaunches } = require('./mergeLaunches'); 35 | 36 | const { FAILED } = testItemStatuses; 37 | 38 | const promiseErrorHandler = (promise, message = '') => 39 | promise.catch((err) => { 40 | console.error(message, err); 41 | }); 42 | 43 | const getInitialTestFinishParams = () => ({ 44 | attributes: [], 45 | description: '', 46 | }); 47 | 48 | class Reporter { 49 | constructor(config) { 50 | const agentInfo = getAgentInfo(); 51 | this.client = new RPClient(config.reporterOptions, agentInfo); 52 | this.testItemIds = new Map(); 53 | this.hooks = new Map(); 54 | this.config = config.reporterOptions; 55 | this.fullCypressConfig = config; 56 | this.videoPromises = []; 57 | 58 | this.currentTestFinishParams = getInitialTestFinishParams(); 59 | 60 | this.currentTestTempInfo = null; 61 | this.suitesStackTempInfo = []; 62 | this.suiteTestCaseIds = new Map(); 63 | // TODO: use a single Map for test info 64 | this.pendingTestsIds = []; 65 | // TODO: use a single Map for suite info 66 | this.suiteStatuses = new Map(); 67 | this.cucumberSteps = new Map(); 68 | } 69 | 70 | saveFullConfig(config) { 71 | this.fullCypressConfig = config; 72 | } 73 | 74 | resetCurrentTestFinishParams() { 75 | this.currentTestFinishParams = getInitialTestFinishParams(); 76 | } 77 | 78 | runStart(launchObj) { 79 | const { tempId, promise } = this.client.startLaunch(launchObj); 80 | const { launch, isLaunchMergeRequired } = this.config; 81 | if (isLaunchMergeRequired) { 82 | createMergeLaunchLockFile(launch, tempId); 83 | } 84 | promiseErrorHandler(promise, 'Fail to start launch'); 85 | this.tempLaunchId = tempId; 86 | } 87 | 88 | runEnd() { 89 | const basePromise = this.config.launchId 90 | ? this.client.getPromiseFinishAllItems(this.tempLaunchId) 91 | : this.client.finishLaunch( 92 | this.tempLaunchId, 93 | Object.assign( 94 | { 95 | endTime: clientHelpers.now(), 96 | }, 97 | this.launchStatus && { status: this.launchStatus }, 98 | ), 99 | ).promise; 100 | 101 | const finishLaunchPromise = Promise.allSettled([basePromise, ...this.videoPromises]) 102 | .then(() => { 103 | const { launch, isLaunchMergeRequired } = this.config; 104 | if (isLaunchMergeRequired) { 105 | deleteMergeLaunchLockFile(launch, this.tempLaunchId); 106 | } 107 | }) 108 | .then(() => { 109 | const { parallel, autoMerge } = this.config; 110 | if (!(parallel && autoMerge)) { 111 | return Promise.resolve(); 112 | } 113 | 114 | return mergeParallelLaunches(this.client, this.config); 115 | }); 116 | return promiseErrorHandler(finishLaunchPromise, 'Fail to finish launch'); 117 | } 118 | 119 | suiteStart(suite) { 120 | const parentId = suite.parentId && this.testItemIds.get(suite.parentId); 121 | const startSuiteObj = getSuiteStartObject(suite); 122 | const { tempId, promise } = this.client.startTestItem( 123 | startSuiteObj, 124 | this.tempLaunchId, 125 | parentId, 126 | ); 127 | promiseErrorHandler(promise, 'Fail to start suite'); 128 | this.testItemIds.set(suite.id, tempId); 129 | this.suitesStackTempInfo.push({ 130 | tempId, 131 | startTime: suite.startTime, 132 | title: suite.title || '', 133 | id: suite.id, 134 | testFileName: suite.testFileName, 135 | }); 136 | } 137 | 138 | suiteEnd(suite) { 139 | const { uploadVideo = false } = this.config; 140 | const { video: isVideoRecordingEnabled = false } = this.fullCypressConfig; 141 | const isRootSuite = 142 | this.suitesStackTempInfo.length && suite.id === this.suitesStackTempInfo[0].id; 143 | 144 | const suiteFinishObj = this.prepareSuiteToFinish(suite); 145 | 146 | if (isVideoRecordingEnabled && uploadVideo && isRootSuite) { 147 | const suiteInfo = this.suitesStackTempInfo[0]; 148 | this.finishSuiteWithVideo(suiteInfo, suiteFinishObj); 149 | } else { 150 | const suiteTempId = this.testItemIds.get(suite.id); 151 | this.finishSuite(suiteFinishObj, suiteTempId); 152 | } 153 | this.suitesStackTempInfo.pop(); 154 | } 155 | 156 | prepareSuiteToFinish(suite) { 157 | const suiteTestCaseId = this.suiteTestCaseIds.get(suite.title); 158 | const suiteStatus = this.suiteStatuses.get(suite.title); 159 | let suiteFinishObj = getSuiteEndObject(suite); 160 | 161 | suiteFinishObj = { 162 | ...suiteFinishObj, 163 | status: suiteStatus || suite.status, 164 | ...(suiteTestCaseId && { testCaseId: suiteTestCaseId }), 165 | }; 166 | 167 | suiteTestCaseId && this.suiteTestCaseIds.delete(suite.title); 168 | suiteStatus && this.suiteStatuses.delete(suite.title); 169 | 170 | return suiteFinishObj; 171 | } 172 | 173 | finishSuite(suiteFinishObj, suiteTempId) { 174 | const finishTestItemPromise = this.client.finishTestItem(suiteTempId, suiteFinishObj).promise; 175 | promiseErrorHandler(finishTestItemPromise, 'Fail to finish suite'); 176 | } 177 | 178 | finishSuiteWithVideo(suiteInfo, suiteFinishObj) { 179 | const uploadVideoForNonFailedSpec = this.config.uploadVideoForNonFailedSpec || false; 180 | const suiteFailed = suiteFinishObj.status === testItemStatuses.FAILED; 181 | 182 | // do not upload video if root suite not failed and uploadVideoForNonFailedSpec is false 183 | if ((!suiteFailed && !uploadVideoForNonFailedSpec) || !suiteInfo.testFileName) { 184 | this.finishSuite(suiteFinishObj, suiteInfo.tempId); 185 | } else { 186 | const sendVideoPromise = this.sendVideo(suiteInfo).finally(() => { 187 | this.finishSuite(suiteFinishObj, suiteInfo.tempId); 188 | }); 189 | this.videoPromises.push(sendVideoPromise); 190 | } 191 | } 192 | 193 | async sendVideo(suiteInfo) { 194 | const { waitForVideoTimeout, waitForVideoInterval, videosFolder, videoCompression } = 195 | this.config; 196 | const { testFileName, tempId, title } = suiteInfo; 197 | const file = await getVideoFile( 198 | testFileName, 199 | videoCompression, 200 | videosFolder, 201 | waitForVideoTimeout, 202 | waitForVideoInterval, 203 | ); 204 | if (!file) { 205 | return null; 206 | } 207 | 208 | const sendVideoPromise = this.client.sendLog( 209 | tempId, 210 | { 211 | message: `Video: '${title}' (${testFileName}.mp4)`, 212 | level: logLevels.INFO, 213 | time: clientHelpers.now(), 214 | }, 215 | file, 216 | ).promise; 217 | promiseErrorHandler(sendVideoPromise, 'Fail to save video'); 218 | 219 | return sendVideoPromise; 220 | } 221 | 222 | testStart(test) { 223 | const parentId = this.testItemIds.get(test.parentId); 224 | const startTestObj = getTestStartObject(test); 225 | const { tempId, promise } = this.client.startTestItem( 226 | startTestObj, 227 | this.tempLaunchId, 228 | parentId, 229 | ); 230 | promiseErrorHandler(promise, 'Fail to start test'); 231 | this.testItemIds.set(test.id, tempId); 232 | this.currentTestTempInfo = { 233 | tempId, 234 | codeRef: test.codeRef, 235 | startTime: startTestObj.startTime, 236 | cucumberStepIds: new Set(), 237 | }; 238 | if (this.pendingTestsIds.includes(test.id)) { 239 | this.testEnd(test); 240 | this.pendingTestsIds = this.pendingTestsIds.filter((id) => id !== test.id); 241 | } 242 | } 243 | 244 | sendLogOnFinishFailedItem(test, tempTestId) { 245 | if (test.status === FAILED) { 246 | const sendFailedLogPromise = this.client.sendLog(tempTestId, { 247 | message: test.err.stack, 248 | level: logLevels.ERROR, 249 | time: clientHelpers.now(), 250 | }).promise; 251 | promiseErrorHandler(sendFailedLogPromise, 'Fail to save error log'); 252 | } 253 | } 254 | 255 | testEnd(test) { 256 | const testId = this.testItemIds.get(test.id); 257 | if (!testId) { 258 | return; 259 | } 260 | this.sendLogOnFinishFailedItem(test, testId); 261 | this.finishFailedStep(test); 262 | const testInfo = Object.assign({}, test, this.currentTestFinishParams); 263 | const finishTestItemPromise = this.client.finishTestItem( 264 | testId, 265 | getTestEndObject(testInfo, this.config.skippedIssue), 266 | ).promise; 267 | promiseErrorHandler(finishTestItemPromise, 'Fail to finish test'); 268 | this.resetCurrentTestFinishParams(); 269 | this.currentTestTempInfo = null; 270 | this.testItemIds.delete(test.id); 271 | } 272 | 273 | testPending(test) { 274 | // if test has not been started, save test.id to finish in testStart(). 275 | // if testStarted() has been called, call testEnd() directly. 276 | if (this.testItemIds.get(test.id)) { 277 | this.testEnd(test); 278 | } else { 279 | this.pendingTestsIds.push(test.id); 280 | } 281 | } 282 | 283 | cucumberStepStart(data) { 284 | const { testStepId, pickleStep } = data; 285 | const parent = this.currentTestTempInfo; 286 | 287 | if (!parent) return; 288 | 289 | const keyword = cucumberKeywordMap[pickleStep.type]; 290 | const stepName = pickleStep.text; 291 | const codeRef = getCodeRef([stepName], parent.codeRef); 292 | 293 | const stepData = { 294 | name: keyword ? `${keyword} ${stepName}` : stepName, 295 | startTime: clientHelpers.now(), 296 | type: entityType.STEP, 297 | codeRef, 298 | hasStats: false, 299 | }; 300 | 301 | const { tempId, promise } = this.client.startTestItem( 302 | stepData, 303 | this.tempLaunchId, 304 | parent.tempId, 305 | ); 306 | promiseErrorHandler(promise, 'Fail to start step'); 307 | this.cucumberSteps.set(testStepId, { tempId, tempParentId: parent.tempId, testStepId }); 308 | parent.cucumberStepIds.add(testStepId); 309 | } 310 | 311 | finishFailedStep(test) { 312 | if (test.status === FAILED) { 313 | const step = this.getCurrentCucumberStep(); 314 | 315 | if (!step) return; 316 | 317 | this.cucumberStepEnd({ 318 | testStepId: step.testStepId, 319 | testStepResult: { 320 | status: testItemStatuses.FAILED, 321 | message: test.err.stack, 322 | }, 323 | }); 324 | } 325 | } 326 | 327 | cucumberStepEnd(data) { 328 | const { testStepId, testStepResult = { status: testItemStatuses.PASSED } } = data; 329 | const step = this.cucumberSteps.get(testStepId); 330 | 331 | if (!step) return; 332 | 333 | if (testStepResult.status === testItemStatuses.FAILED) { 334 | this.sendLog(step.tempId, { 335 | time: clientHelpers.now(), 336 | level: logLevels.ERROR, 337 | message: testStepResult.message, 338 | }); 339 | } 340 | 341 | this.client.finishTestItem(step.tempId, { 342 | status: testStepResult.status, 343 | endTime: clientHelpers.now(), 344 | }); 345 | 346 | this.cucumberSteps.delete(testStepId); 347 | if (this.currentTestTempInfo) { 348 | this.currentTestTempInfo.cucumberStepIds.delete(testStepId); 349 | } 350 | } 351 | 352 | hookStart(hook) { 353 | const hookStartObject = getHookStartObject(hook); 354 | switch (hookStartObject.type) { 355 | case entityType.BEFORE_SUITE: 356 | hookStartObject.startTime = this.getCurrentSuiteInfo().startTime - 1; 357 | break; 358 | case entityType.BEFORE_METHOD: 359 | hookStartObject.startTime = this.currentTestTempInfo 360 | ? this.currentTestTempInfo.startTime - 1 361 | : hookStartObject.startTime; 362 | break; 363 | default: 364 | break; 365 | } 366 | this.hooks.set(hook.id, hookStartObject); 367 | } 368 | 369 | hookEnd(hook) { 370 | const startedHook = this.hooks.get(hook.id); 371 | if (!startedHook) return; 372 | const { tempId, promise } = this.client.startTestItem( 373 | startedHook, 374 | this.tempLaunchId, 375 | this.testItemIds.get(hook.parentId), 376 | ); 377 | promiseErrorHandler(promise, 'Fail to start hook'); 378 | this.sendLogOnFinishFailedItem(hook, tempId); 379 | const finishHookPromise = this.client.finishTestItem(tempId, { 380 | status: hook.status, 381 | endTime: clientHelpers.now(), 382 | }).promise; 383 | this.hooks.delete(hook.id); 384 | promiseErrorHandler(finishHookPromise, 'Fail to finish hook'); 385 | } 386 | 387 | getCurrentSuiteInfo() { 388 | return this.suitesStackTempInfo.length 389 | ? this.suitesStackTempInfo[this.suitesStackTempInfo.length - 1] 390 | : undefined; 391 | } 392 | 393 | getCurrentSuiteId() { 394 | const currentSuiteInfo = this.getCurrentSuiteInfo(); 395 | return currentSuiteInfo && currentSuiteInfo.tempId; 396 | } 397 | 398 | getCurrentCucumberStep() { 399 | if (this.currentTestTempInfo && this.currentTestTempInfo.cucumberStepIds.size > 0) { 400 | const testStepId = Array.from(this.currentTestTempInfo.cucumberStepIds.values())[ 401 | this.currentTestTempInfo.cucumberStepIds.size - 1 402 | ]; 403 | 404 | return this.cucumberSteps.get(testStepId); 405 | } 406 | 407 | return null; 408 | } 409 | 410 | getCurrentCucumberStepId() { 411 | const step = this.getCurrentCucumberStep(); 412 | 413 | return step && step.tempId; 414 | } 415 | 416 | sendLog(tempId, { level, message = '', file }) { 417 | return this.client.sendLog( 418 | tempId, 419 | { 420 | message, 421 | level, 422 | time: clientHelpers.now(), 423 | }, 424 | file, 425 | ).promise; 426 | } 427 | 428 | sendLogToCurrentItem(log) { 429 | const tempItemId = 430 | this.getCurrentCucumberStepId() || 431 | (this.currentTestTempInfo && this.currentTestTempInfo.tempId) || 432 | this.getCurrentSuiteId(); 433 | if (tempItemId) { 434 | const promise = this.sendLog(tempItemId, log); 435 | promiseErrorHandler(promise, 'Fail to send log to current item'); 436 | } 437 | } 438 | 439 | sendLaunchLog(log) { 440 | const promise = this.sendLog(this.tempLaunchId, log); 441 | promiseErrorHandler(promise, 'Fail to send launch log'); 442 | } 443 | 444 | addAttributes(attributes) { 445 | this.currentTestFinishParams.attributes = this.currentTestFinishParams.attributes.concat( 446 | attributes || [], 447 | ); 448 | } 449 | 450 | setDescription(description) { 451 | this.currentTestFinishParams.description = description; 452 | } 453 | 454 | setTestCaseId({ testCaseId, suiteTitle }) { 455 | if (suiteTitle) { 456 | this.suiteTestCaseIds.set(suiteTitle, testCaseId); 457 | } else { 458 | Object.assign(this.currentTestFinishParams, testCaseId && { testCaseId }); 459 | } 460 | } 461 | 462 | setTestItemStatus({ status, suiteTitle }) { 463 | if (suiteTitle) { 464 | this.suiteStatuses.set(suiteTitle, status); 465 | const rootSuite = this.suitesStackTempInfo.length && this.suitesStackTempInfo[0]; 466 | if (rootSuite && status === testItemStatuses.FAILED) { 467 | this.suitesStackTempInfo[0].status = status; 468 | } 469 | } else { 470 | Object.assign(this.currentTestFinishParams, status && { status }); 471 | } 472 | } 473 | 474 | setLaunchStatus({ status }) { 475 | this.launchStatus = status; 476 | } 477 | 478 | async sendScreenshot(screenshotInfo, logMessage) { 479 | const tempItemId = this.currentTestTempInfo && this.currentTestTempInfo.tempId; 480 | const fileName = screenshotInfo.path; 481 | 482 | if (!fileName || !tempItemId) { 483 | return; 484 | } 485 | 486 | const level = fileName && fileName.includes('(failed)') ? logLevels.ERROR : logLevels.INFO; 487 | const file = await getScreenshotAttachment(fileName); 488 | if (!file) { 489 | return; 490 | } 491 | 492 | const message = logMessage || `screenshot ${file.name}`; 493 | 494 | const sendScreenshotPromise = this.client.sendLog( 495 | tempItemId, 496 | { 497 | message, 498 | level, 499 | time: new Date(screenshotInfo.takenAt).valueOf(), 500 | }, 501 | file, 502 | ).promise; 503 | promiseErrorHandler(sendScreenshotPromise, 'Fail to save screenshot.'); 504 | } 505 | } 506 | 507 | module.exports = Reporter; 508 | -------------------------------------------------------------------------------- /lib/testStatuses.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const RP_STATUSES = { 18 | PASSED: 'passed', 19 | FAILED: 'failed', 20 | SKIPPED: 'skipped', 21 | STOPPED: 'stopped', 22 | INTERRUPTED: 'interrupted', 23 | CANCELLED: 'cancelled', 24 | INFO: 'info', 25 | WARN: 'warn', 26 | }; 27 | 28 | module.exports = { 29 | RP_STATUSES, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/utils/attachments.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const glob = require('glob'); 19 | const path = require('path'); 20 | const ffmpeg = require('fluent-ffmpeg'); 21 | const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg'); 22 | const ffprobeStatic = require('ffprobe-static'); 23 | 24 | ffmpeg.setFfmpegPath(ffmpegInstaller.path); 25 | ffmpeg.setFfprobePath(ffprobeStatic.path); 26 | 27 | const fsPromises = fs.promises; 28 | 29 | const DEFAULT_WAIT_FOR_FILE_TIMEOUT = 10000; 30 | const DEFAULT_WAIT_FOR_FILE_INTERVAL = 500; 31 | const DEFAULT_CRF = 32; 32 | const MIN_CRF = 1; 33 | const MAX_CRF = 52; 34 | 35 | const getScreenshotAttachment = async (absolutePath) => { 36 | if (!absolutePath) return absolutePath; 37 | const name = absolutePath.split(path.sep).pop(); 38 | return { 39 | name, 40 | type: 'image/png', 41 | content: await fsPromises.readFile(absolutePath, { encoding: 'base64' }), 42 | }; 43 | }; 44 | 45 | async function getFilePathByGlobPattern(globFilePattern) { 46 | const files = await glob.glob(globFilePattern); 47 | 48 | if (files.length) { 49 | return files[0]; 50 | } 51 | 52 | return null; 53 | } 54 | /* 55 | * The moov atom in an MP4 file is a crucial part of the file’s structure. It contains metadata about the video, such as the duration, display characteristics, and timing information. 56 | * Function check for the moov atom in file content and ensure is video file ready. 57 | */ 58 | const checkVideoFileReady = async (videoFilePath) => { 59 | try { 60 | const fileData = await fsPromises.readFile(videoFilePath); 61 | 62 | if (fileData.includes('moov')) { 63 | return true; 64 | } 65 | } catch (e) { 66 | throw new Error(`Error reading file: ${e.message}`); 67 | } 68 | 69 | return false; 70 | }; 71 | 72 | const waitForVideoFile = ( 73 | globFilePattern, 74 | timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, 75 | interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, 76 | ) => 77 | new Promise((resolve, reject) => { 78 | let filePath = null; 79 | let totalFileWaitingTime = 0; 80 | 81 | async function checkFileExistsAndReady() { 82 | if (!filePath) { 83 | filePath = await getFilePathByGlobPattern(globFilePattern); 84 | } 85 | let isVideoFileReady = false; 86 | 87 | if (filePath) { 88 | isVideoFileReady = await checkVideoFileReady(filePath); 89 | } 90 | 91 | if (isVideoFileReady) { 92 | resolve(filePath); 93 | } else if (totalFileWaitingTime >= timeout) { 94 | reject( 95 | new Error( 96 | `Timeout of ${timeout}ms reached, file ${globFilePattern} not found or not ready yet.`, 97 | ), 98 | ); 99 | } else { 100 | totalFileWaitingTime += interval; 101 | setTimeout(checkFileExistsAndReady, interval); 102 | } 103 | } 104 | 105 | checkFileExistsAndReady().catch(reject); 106 | }); 107 | 108 | const compressVideo = (filePath, crfValue) => { 109 | return new Promise((resolve, reject) => { 110 | const outputFilePath = path.join( 111 | path.dirname(filePath), 112 | `compressed_${path.basename(filePath)}`, 113 | ); 114 | 115 | ffmpeg(filePath) 116 | .outputOptions(`-crf ${crfValue}`) 117 | .save(outputFilePath) 118 | .on('end', () => { 119 | resolve(outputFilePath); 120 | }) 121 | .on('error', (err) => { 122 | reject(err); 123 | }); 124 | }); 125 | }; 126 | 127 | const getVideoFile = async ( 128 | specFileName, 129 | videoCompression = false, 130 | videosFolder = '**', 131 | timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, 132 | interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, 133 | ) => { 134 | if (!specFileName) { 135 | return null; 136 | } 137 | const fileName = specFileName.toLowerCase().endsWith('.mp4') 138 | ? specFileName 139 | : `${specFileName}.mp4`; 140 | const globFilePath = `**/${videosFolder}/${fileName}`; 141 | let videoFilePath; 142 | 143 | try { 144 | videoFilePath = await waitForVideoFile(globFilePath, timeout, interval); 145 | 146 | if (typeof videoCompression === 'boolean' && videoCompression) { 147 | videoFilePath = await compressVideo(videoFilePath, DEFAULT_CRF); 148 | } else if ( 149 | typeof videoCompression === 'number' && 150 | videoCompression >= MIN_CRF && 151 | videoCompression < MAX_CRF 152 | ) { 153 | videoFilePath = await compressVideo(videoFilePath, videoCompression); 154 | } 155 | } catch (e) { 156 | console.warn(e.message); 157 | return null; 158 | } 159 | 160 | return { 161 | name: fileName, 162 | type: 'video/mp4', 163 | content: await fsPromises.readFile(videoFilePath, { encoding: 'base64' }), 164 | }; 165 | }; 166 | 167 | module.exports = { 168 | getScreenshotAttachment, 169 | getVideoFile, 170 | waitForVideoFile, 171 | getFilePathByGlobPattern, 172 | checkVideoFileReady, 173 | compressVideo, 174 | }; 175 | -------------------------------------------------------------------------------- /lib/utils/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const getCodeRef = (testItemPath, testFileName) => 18 | `${testFileName.replace(/\\/g, '/')}/${testItemPath.join('/')}`; 19 | 20 | module.exports = { 21 | getCodeRef, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const attachmentUtils = require('./attachments'); 18 | const commonUtils = require('./common'); 19 | const objectCreators = require('./objectCreators'); 20 | const specCountCalculation = require('./specCountCalculation'); 21 | 22 | module.exports = { 23 | ...attachmentUtils, 24 | ...commonUtils, 25 | ...objectCreators, 26 | ...specCountCalculation, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/utils/objectCreators.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | const clientHelpers = require('@reportportal/client-javascript/lib/helpers'); 19 | 20 | const pjson = require('../../package.json'); 21 | const { entityType, hookTypesMap, testItemStatuses } = require('../constants'); 22 | const { getCodeRef } = require('./common'); 23 | 24 | const { FAILED, PASSED, SKIPPED } = testItemStatuses; 25 | 26 | const getAgentInfo = () => ({ 27 | version: pjson.version, 28 | name: pjson.name, 29 | }); 30 | 31 | const getSystemAttributes = (config) => { 32 | const agentInfo = getAgentInfo(); 33 | const systemAttributes = [ 34 | { 35 | key: 'agent', 36 | value: `${agentInfo.name}|${agentInfo.version}`, 37 | system: true, 38 | }, 39 | ]; 40 | if (config.reporterOptions.skippedIssue === false) { 41 | const skippedIssueAttribute = { 42 | key: 'skippedIssue', 43 | value: 'false', 44 | system: true, 45 | }; 46 | systemAttributes.push(skippedIssueAttribute); 47 | } 48 | return systemAttributes; 49 | }; 50 | 51 | const getConfig = (initialConfig) => { 52 | const attributes = initialConfig.reporterOptions.attributes || []; 53 | 54 | if ( 55 | initialConfig.reporterOptions.parallel && 56 | initialConfig.reporterOptions.autoMerge && 57 | process.env.CI_BUILD_ID 58 | ) { 59 | attributes.push({ value: process.env.CI_BUILD_ID }); 60 | } 61 | 62 | const { apiKey, token, ...reporterOptions } = initialConfig.reporterOptions; 63 | 64 | let calculatedApiKey = process.env.RP_API_KEY || apiKey; 65 | if (!calculatedApiKey) { 66 | calculatedApiKey = process.env.RP_TOKEN || token; 67 | if (calculatedApiKey) { 68 | console.warn('ReportPortal warning. Option "token" is deprecated. Use "apiKey" instead.'); 69 | } 70 | } 71 | 72 | return { 73 | ...initialConfig, 74 | reporterOptions: { 75 | ...reporterOptions, 76 | attributes, 77 | apiKey: calculatedApiKey, 78 | }, 79 | }; 80 | }; 81 | 82 | const getLaunchStartObject = (config) => { 83 | const launchAttributes = (config.reporterOptions.attributes || []).concat( 84 | getSystemAttributes(config), 85 | ); 86 | 87 | return { 88 | launch: config.reporterOptions.launch, 89 | description: config.reporterOptions.description, 90 | attributes: launchAttributes, 91 | rerun: config.reporterOptions.rerun, 92 | rerunOf: config.reporterOptions.rerunOf, 93 | mode: config.reporterOptions.mode, 94 | startTime: clientHelpers.now(), 95 | id: config.reporterOptions.launchId, 96 | }; 97 | }; 98 | 99 | const getSuiteStartInfo = (suite, testFileName) => ({ 100 | id: suite.id, 101 | title: suite.title, 102 | startTime: clientHelpers.now(), 103 | description: suite.description, 104 | codeRef: getCodeRef(suite.titlePath(), testFileName), 105 | parentId: !suite.root ? suite.parent.id : undefined, 106 | testFileName: testFileName.split(path.sep).pop(), 107 | }); 108 | 109 | const getSuiteEndInfo = (suite) => { 110 | let failed = false; 111 | if (suite.tests != null) { 112 | failed = suite.tests.some((test) => test.state === testItemStatuses.FAILED); 113 | } 114 | return { 115 | id: suite.id, 116 | status: failed ? testItemStatuses.FAILED : undefined, 117 | title: suite.title, 118 | endTime: clientHelpers.now(), 119 | }; 120 | }; 121 | 122 | const getSuiteStartObject = (suite) => ({ 123 | type: entityType.SUITE, 124 | name: suite.title.slice(0, 255).toString(), 125 | startTime: suite.startTime, 126 | description: suite.description, 127 | codeRef: suite.codeRef, 128 | attributes: [], 129 | }); 130 | 131 | const getSuiteEndObject = (suite) => ({ 132 | status: suite.status, 133 | endTime: suite.endTime, 134 | }); 135 | 136 | const getTestInfo = (test, testFileName, status, err) => ({ 137 | id: test.id, 138 | status: status || (test.state === 'pending' ? testItemStatuses.SKIPPED : test.state), 139 | title: test.title, 140 | codeRef: getCodeRef(test.titlePath(), testFileName), 141 | parentId: test.parent.id, 142 | err: err || test.err, 143 | testFileName, 144 | }); 145 | 146 | const getTestStartObject = (test) => ({ 147 | type: entityType.STEP, 148 | name: test.title.slice(0, 255).toString(), 149 | startTime: clientHelpers.now(), 150 | codeRef: test.codeRef, 151 | attributes: [], 152 | }); 153 | 154 | const getTestEndObject = (testInfo, skippedIssue) => { 155 | const testEndObj = Object.assign( 156 | { 157 | endTime: clientHelpers.now(), 158 | status: testInfo.status, 159 | attributes: testInfo.attributes, 160 | description: testInfo.description, 161 | }, 162 | testInfo.testCaseId && { testCaseId: testInfo.testCaseId }, 163 | ); 164 | if (testInfo.status === SKIPPED && skippedIssue === false) { 165 | testEndObj.issue = { 166 | issueType: 'NOT_ISSUE', 167 | }; 168 | } 169 | return testEndObj; 170 | }; 171 | 172 | const getHookInfo = (hook, testFileName, status, err) => { 173 | const hookRPType = hookTypesMap[hook.hookName]; 174 | let parentId = hook.parent.id; 175 | if ([entityType.BEFORE_SUITE, entityType.AFTER_SUITE].includes(hookRPType)) { 176 | parentId = hook.parent.parent && hook.parent.parent.title ? hook.parent.parent.id : undefined; 177 | } 178 | return { 179 | id: hook.failedFromHookId ? `${hook.failedFromHookId}_${hook.id}` : `${hook.hookId}_${hook.id}`, 180 | hookName: hook.hookName, 181 | title: hook.title, 182 | status: status || (hook.state === FAILED ? FAILED : PASSED), 183 | parentId, 184 | codeRef: getCodeRef(hook.titlePath(), testFileName), 185 | err: (err && err.message) || err || (hook.err && hook.err.message), 186 | testFileName, 187 | }; 188 | }; 189 | 190 | const getHookStartObject = (hook) => { 191 | const hookRPType = hookTypesMap[hook.hookName]; 192 | const hookName = hook.title.replace(`"${hook.hookName}" hook:`, '').trim(); 193 | return { 194 | name: hookName, 195 | startTime: clientHelpers.now(), 196 | type: hookRPType, 197 | codeRef: hook.codeRef, 198 | }; 199 | }; 200 | 201 | module.exports = { 202 | getAgentInfo, 203 | getSystemAttributes, 204 | getConfig, 205 | getLaunchStartObject, 206 | getSuiteStartObject, 207 | getSuiteEndObject, 208 | getTestStartObject, 209 | getTestEndObject, 210 | getHookStartObject, 211 | // there are utils to preprocess Mocha entities 212 | getTestInfo, 213 | getSuiteStartInfo, 214 | getSuiteEndInfo, 215 | getHookInfo, 216 | }; 217 | -------------------------------------------------------------------------------- /lib/utils/specCountCalculation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const glob = require('glob'); 18 | const path = require('path'); 19 | const minimatch = require('minimatch'); 20 | 21 | const getFixtureFolderPattern = (config) => { 22 | return [].concat(config.fixturesFolder ? path.join(config.fixturesFolder, '**', '*') : []); 23 | }; 24 | 25 | const getExcludeSpecPattern = (config) => { 26 | // Return cypress >= 10 pattern. 27 | if (config.excludeSpecPattern) { 28 | const excludePattern = Array.isArray(config.excludeSpecPattern) 29 | ? config.excludeSpecPattern 30 | : [config.excludeSpecPattern]; 31 | return [...excludePattern]; 32 | } 33 | 34 | // Return cypress <= 9 pattern 35 | const ignoreTestFilesPattern = Array.isArray(config.ignoreTestFiles) 36 | ? config.ignoreTestFiles 37 | : [config.ignoreTestFiles] || []; 38 | 39 | return [...ignoreTestFilesPattern]; 40 | }; 41 | 42 | const getSpecPattern = (config) => { 43 | if (config.specPattern) return [].concat(config.specPattern); 44 | 45 | return Array.isArray(config.testFiles) 46 | ? config.testFiles.map((file) => `${config.integrationFolder}/${file}`) 47 | : [].concat(`${config.integrationFolder}/${config.testFiles}`); 48 | }; 49 | 50 | const getTotalSpecs = (config) => { 51 | if (!config.testFiles && !config.specPattern) 52 | throw new Error('Configuration property not set! Neither for cypress <= 9 nor cypress >= 10'); 53 | 54 | const specPattern = getSpecPattern(config); 55 | 56 | const excludeSpecPattern = getExcludeSpecPattern(config); 57 | 58 | const options = { 59 | sort: true, 60 | absolute: true, 61 | nodir: true, 62 | ignore: [config.supportFile].concat(getFixtureFolderPattern(config)), 63 | }; 64 | 65 | const doesNotMatchAllIgnoredPatterns = (file) => 66 | excludeSpecPattern.every( 67 | (pattern) => !minimatch(file, pattern, { dot: true, matchBase: true }), 68 | ); 69 | 70 | const globResult = specPattern.reduce( 71 | (files, pattern) => files.concat(glob.sync(pattern, options) || []), 72 | [], 73 | ); 74 | 75 | return globResult.filter(doesNotMatchAllIgnoredPatterns).length; 76 | }; 77 | 78 | module.exports = { 79 | getTotalSpecs, 80 | getExcludeSpecPattern, 81 | getFixtureFolderPattern, 82 | getSpecPattern, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const Mocha = require('mocha'); 18 | const ReportPortalReporter = require('./reporter'); 19 | const { reporterEvents } = require('./constants'); 20 | 21 | const { 22 | EVENT_RUN_BEGIN, 23 | EVENT_RUN_END, 24 | EVENT_TEST_BEGIN, 25 | EVENT_TEST_END, 26 | EVENT_SUITE_BEGIN, 27 | EVENT_SUITE_END, 28 | EVENT_HOOK_BEGIN, 29 | EVENT_HOOK_END, 30 | EVENT_TEST_PENDING, 31 | } = Mocha.Runner.constants; 32 | 33 | const interval = setInterval(() => {}, 1000); 34 | let reporter; 35 | process.on('message', (message) => { 36 | const { event } = message; 37 | switch (event) { 38 | case reporterEvents.INIT: 39 | reporter = new ReportPortalReporter(message.config); 40 | break; 41 | case reporterEvents.FULL_CONFIG: 42 | reporter.saveFullConfig(message.config); 43 | break; 44 | case EVENT_RUN_BEGIN: 45 | reporter.runStart(message.launch); 46 | break; 47 | case EVENT_RUN_END: 48 | reporter 49 | .runEnd() 50 | .then(() => { 51 | interval && clearInterval(interval); 52 | process.exit(0); 53 | }) 54 | .catch((err) => { 55 | console.error(err); 56 | interval && clearInterval(interval); 57 | process.exit(1); 58 | }); 59 | break; 60 | case EVENT_SUITE_BEGIN: 61 | reporter.suiteStart(message.suite); 62 | break; 63 | case EVENT_SUITE_END: 64 | reporter.suiteEnd(message.suite); 65 | break; 66 | case EVENT_TEST_BEGIN: 67 | reporter.testStart(message.test); 68 | break; 69 | case EVENT_TEST_END: 70 | reporter.testEnd(message.test); 71 | break; 72 | case EVENT_TEST_PENDING: 73 | reporter.testPending(message.test); 74 | break; 75 | case EVENT_HOOK_BEGIN: 76 | reporter.hookStart(message.hook); 77 | break; 78 | case EVENT_HOOK_END: 79 | reporter.hookEnd(message.hook); 80 | break; 81 | case reporterEvents.LOG: 82 | reporter.sendLogToCurrentItem(message.log); 83 | break; 84 | case reporterEvents.LAUNCH_LOG: 85 | reporter.sendLaunchLog(message.log); 86 | break; 87 | case reporterEvents.ADD_ATTRIBUTES: 88 | reporter.addAttributes(message.attributes); 89 | break; 90 | case reporterEvents.SET_DESCRIPTION: 91 | reporter.setDescription(message.description); 92 | break; 93 | case reporterEvents.SET_TEST_CASE_ID: 94 | reporter.setTestCaseId(message.testCaseIdInfo); 95 | break; 96 | case reporterEvents.SCREENSHOT: 97 | reporter.sendScreenshot(message.details.screenshotInfo, message.details.logMessage); 98 | break; 99 | case reporterEvents.SET_STATUS: 100 | reporter.setTestItemStatus(message.statusInfo); 101 | break; 102 | case reporterEvents.SET_LAUNCH_STATUS: 103 | reporter.setLaunchStatus(message.statusInfo); 104 | break; 105 | case reporterEvents.CUCUMBER_STEP_START: 106 | reporter.cucumberStepStart(message.step); 107 | break; 108 | case reporterEvents.CUCUMBER_STEP_END: 109 | reporter.cucumberStepEnd(message.step); 110 | break; 111 | default: 112 | break; 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reportportal/agent-js-cypress", 3 | "version": "5.5.0", 4 | "description": "This agent helps Cypress to communicate with ReportPortal", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint . --quiet", 8 | "format": "npm run lint -- --fix", 9 | "test": "jest --detectOpenHandles --config ./jest.config.js", 10 | "test:coverage": "jest --coverage" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/reportportal/agent-js-cypress" 15 | }, 16 | "dependencies": { 17 | "@reportportal/client-javascript": "~5.4.0", 18 | "glob": "^9.3.5", 19 | "minimatch": "^3.1.2", 20 | "mocha": "^10.2.0", 21 | "node-ipc": "9.1.1", 22 | "@ffmpeg-installer/ffmpeg": "^1.1.0", 23 | "ffmpeg": "^0.0.4", 24 | "ffprobe-static": "^3.1.0", 25 | "fluent-ffmpeg": "^2.1.3" 26 | }, 27 | "directories": { 28 | "lib": "./lib" 29 | }, 30 | "files": [ 31 | "/lib" 32 | ], 33 | "engines": { 34 | "node": ">=14.x" 35 | }, 36 | "license": "Apache-2.0", 37 | "devDependencies": { 38 | "@types/jest": "^29.5.12", 39 | "cypress": "^14.2.1", 40 | "eslint": "^8.57.0", 41 | "eslint-config-airbnb-base": "^15.0.0", 42 | "eslint-config-prettier": "^8.10.0", 43 | "eslint-plugin-cypress": "2.15.2", 44 | "eslint-plugin-import": "^2.29.1", 45 | "eslint-plugin-jest": "^23.20.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "jest": "^29.7.0", 48 | "mock-fs": "^4.14.0", 49 | "prettier": "^2.8.8" 50 | }, 51 | "keywords": [ 52 | "ReportPortal", 53 | "rp", 54 | "Cypress", 55 | "reporting", 56 | "reporter", 57 | "epam" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /test/mergeLaunches.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const mergeLaunches = require('./../lib/mergeLaunches'); 18 | const mergeLaunchesUtils = require('./../lib/mergeLaunchesUtils'); 19 | 20 | describe('mergeLaunches', () => { 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | it('no launches in progress: should call callClientMergeLaunches immediately', () => { 25 | jest.spyOn(mergeLaunchesUtils, 'isLaunchesInProgress').mockImplementation(() => false); 26 | const spyCallClientMergeLaunches = jest 27 | .spyOn(mergeLaunchesUtils, 'callClientMergeLaunches') 28 | .mockImplementation(() => {}); 29 | const launch = 'foo-launchName'; 30 | 31 | mergeLaunches.mergeLaunches({ launch }); 32 | 33 | expect(spyCallClientMergeLaunches).toHaveBeenCalled(); 34 | }); 35 | 36 | it('launches will stop in 5 ms: should return promise', () => { 37 | jest.spyOn(mergeLaunchesUtils, 'isLaunchesInProgress').mockImplementation(() => true); 38 | 39 | setTimeout(() => { 40 | jest.spyOn(mergeLaunchesUtils, 'isLaunchesInProgress').mockImplementation(() => false); 41 | }, 1); 42 | 43 | const spyCallClientMergeLaunches = jest 44 | .spyOn(mergeLaunchesUtils, 'callClientMergeLaunches') 45 | .mockImplementation(() => Promise.resolve()); 46 | const launch = 'foo-launchName'; 47 | 48 | const promise = mergeLaunches.mergeLaunches({ launch }); 49 | 50 | expect(spyCallClientMergeLaunches).not.toHaveBeenCalled(); 51 | 52 | expect(promise.then).toBeDefined(); 53 | 54 | return promise.then(() => expect(spyCallClientMergeLaunches).toHaveBeenCalled()); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/mergeLaunchesUtils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const mockFS = require('mock-fs'); 19 | 20 | const mergeLaunchesUtils = require('./../lib/mergeLaunchesUtils'); 21 | 22 | describe('merge launches script', () => { 23 | describe('getLaunchLockFileName', () => { 24 | afterEach(() => { 25 | jest.clearAllMocks(); 26 | }); 27 | it('should return file name with launch name and temp id', () => { 28 | const launchName = 'launchName'; 29 | const tempID = 'tempId'; 30 | const expectedFileName = 'rplaunchinprogress-launchName-tempId.tmp'; 31 | 32 | const fileName = mergeLaunchesUtils.getLaunchLockFileName(launchName, tempID); 33 | 34 | expect(fileName).toEqual(expectedFileName); 35 | }); 36 | }); 37 | 38 | describe('createMergeLaunchLockFile', () => { 39 | it('should create lock file', () => { 40 | const spyFSOpen = jest.spyOn(fs, 'open').mockImplementation(() => {}); 41 | const launchName = 'launchName'; 42 | const tempID = 'tempId'; 43 | 44 | mergeLaunchesUtils.createMergeLaunchLockFile(launchName, tempID); 45 | 46 | expect(spyFSOpen).toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | describe('deleteMergeLaunchLockFile', () => { 51 | it('should delete lock file', () => { 52 | const spyFSOpen = jest.spyOn(fs, 'unlink').mockImplementation(() => {}); 53 | const launchName = 'launchName'; 54 | const tempID = 'tempId'; 55 | 56 | mergeLaunchesUtils.deleteMergeLaunchLockFile(launchName, tempID); 57 | 58 | expect(spyFSOpen).toHaveBeenCalled(); 59 | }); 60 | }); 61 | 62 | describe('isLaunchesInProgress', () => { 63 | afterEach(() => { 64 | mockFS.restore(); 65 | }); 66 | it('should return true if lock files exist', () => { 67 | mockFS({ 68 | 'rplaunchinprogress-launchName-tempId.tmp': '', 69 | }); 70 | const launchName = 'launchName'; 71 | 72 | const isInProgress = mergeLaunchesUtils.isLaunchesInProgress(launchName); 73 | 74 | expect(isInProgress).toEqual(true); 75 | }); 76 | 77 | it("should return false if lock files don't exist", () => { 78 | mockFS({ 79 | 'foo-launchName.tmp': '', 80 | }); 81 | const launchName = 'launchName'; 82 | 83 | const isInProgress = mergeLaunchesUtils.isLaunchesInProgress(launchName); 84 | 85 | expect(isInProgress).toEqual(false); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/mock/mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const currentDate = new Date().valueOf(); 18 | 19 | class RPClient { 20 | constructor(config) { 21 | this.config = config; 22 | this.headers = { 23 | foo: 'bar', 24 | }; 25 | this.startLaunch = jest.fn().mockReturnValue({ 26 | promise: Promise.resolve('ok'), 27 | tempId: 'tempLaunchId', 28 | }); 29 | 30 | this.finishLaunch = jest.fn().mockReturnValue({ 31 | promise: Promise.resolve('ok'), 32 | }); 33 | 34 | this.startTestItem = jest.fn().mockReturnValue({ 35 | promise: Promise.resolve('ok'), 36 | tempId: 'testItemId', 37 | }); 38 | 39 | this.finishTestItem = jest.fn().mockReturnValue({ 40 | promise: Promise.resolve('ok'), 41 | }); 42 | 43 | this.sendLog = jest.fn().mockReturnValue({ 44 | promise: Promise.resolve('ok'), 45 | }); 46 | } 47 | } 48 | 49 | const getDefaultConfig = () => ({ 50 | reporter: '@reportportal/agent-js-cypress', 51 | reporterOptions: { 52 | apiKey: 'reportportalApiKey', 53 | endpoint: 'https://reportportal.server/api/v1', 54 | project: 'ProjectName', 55 | launch: 'LauncherName', 56 | description: 'Launch description', 57 | attributes: [], 58 | }, 59 | }); 60 | 61 | const RealDate = Date; 62 | 63 | const MockedDate = (...attrs) => 64 | attrs.length ? new RealDate(...attrs) : new RealDate(currentDate); 65 | 66 | module.exports = { 67 | RPClient, 68 | getDefaultConfig, 69 | MockedDate, 70 | RealDate, 71 | currentDate, 72 | }; 73 | -------------------------------------------------------------------------------- /test/utils/attachments.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fsPromises = require('fs/promises'); 18 | const mockFs = require('mock-fs'); 19 | const path = require('path'); 20 | const glob = require('glob'); 21 | 22 | jest.mock('fluent-ffmpeg'); 23 | const ffmpeg = require('fluent-ffmpeg'); 24 | const attachmentUtils = require('../../lib/utils/attachments'); 25 | 26 | const { 27 | getScreenshotAttachment, 28 | getVideoFile, 29 | waitForVideoFile, 30 | getFilePathByGlobPattern, 31 | checkVideoFileReady, 32 | compressVideo, 33 | } = attachmentUtils; 34 | 35 | const sep = path.sep; 36 | 37 | describe('attachment utils', () => { 38 | describe('getScreenshotAttachment', () => { 39 | beforeEach(() => { 40 | mockFs({ 41 | '/example/screenshots/example.spec.js': { 42 | 'suite name -- test name (failed).png': Buffer.from([8, 6, 7, 5, 3, 0, 9]), 43 | 'suite name -- test name.png': Buffer.from([1, 2, 3, 4, 5, 6, 7]), 44 | 'suite name -- test name (1).png': Buffer.from([8, 7, 6, 5, 4, 3, 2]), 45 | 'customScreenshot1.png': Buffer.from([1, 1, 1, 1, 1, 1, 1]), 46 | }, 47 | }); 48 | }); 49 | 50 | afterEach(() => { 51 | mockFs.restore(); 52 | }); 53 | 54 | it('getScreenshotAttachment: should not fail on undefined', async () => { 55 | const testFile = undefined; 56 | const attachment = await getScreenshotAttachment(testFile); 57 | expect(attachment).not.toBeDefined(); 58 | }); 59 | 60 | it('getScreenshotAttachment: should return attachment for absolute path', async () => { 61 | const testFile = `${sep}example${sep}screenshots${sep}example.spec.js${sep}suite name -- test name (failed).png`; 62 | const expectedAttachment = { 63 | name: 'suite name -- test name (failed).png', 64 | type: 'image/png', 65 | content: Buffer.from([8, 6, 7, 5, 3, 0, 9]).toString('base64'), 66 | }; 67 | 68 | const attachment = await getScreenshotAttachment(testFile); 69 | 70 | expect(attachment).toBeDefined(); 71 | expect(attachment).toEqual(expectedAttachment); 72 | }); 73 | }); 74 | 75 | describe('getFilePathByGlobPattern', () => { 76 | beforeEach(() => { 77 | jest.clearAllMocks(); 78 | }); 79 | 80 | test('returns the path of the first file if files are found', async () => { 81 | const mockFiles = ['path/to/first/file.mp4', 'path/to/second/file.mp4']; 82 | jest.spyOn(glob, 'glob').mockResolvedValueOnce(mockFiles); 83 | 84 | const result = await getFilePathByGlobPattern('*.mp4'); 85 | expect(result).toBe('path/to/first/file.mp4'); 86 | }); 87 | 88 | test('returns null if no files are found', async () => { 89 | jest.spyOn(glob, 'glob').mockResolvedValueOnce([]); 90 | 91 | const result = await getFilePathByGlobPattern('*.mp4'); 92 | expect(result).toBeNull(); 93 | }); 94 | }); 95 | 96 | describe('checkVideoFileReady', () => { 97 | beforeEach(() => { 98 | jest.clearAllMocks(); 99 | }); 100 | 101 | test('returns true if the video file contains "moov" atom', async () => { 102 | const mockFileData = Buffer.from('some data with moov in it'); 103 | jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData); 104 | 105 | const result = await checkVideoFileReady('path/to/video.mp4'); 106 | expect(result).toBe(true); 107 | }); 108 | 109 | test('returns false if the video file does not contain "moov" atom', async () => { 110 | const mockFileData = Buffer.from('some data without the keyword'); 111 | jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData); 112 | 113 | const result = await checkVideoFileReady('path/to/video.mp4'); 114 | expect(result).toBe(false); 115 | }); 116 | 117 | test('throws an error if there is an error reading the file', async () => { 118 | jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file')); 119 | 120 | await expect(checkVideoFileReady('path/to/video.mp4')).rejects.toThrow( 121 | 'Error reading file: Failed to read file', 122 | ); 123 | }); 124 | }); 125 | 126 | // TODO: Fix the tests 127 | describe.skip('waitForVideoFile', () => { 128 | beforeEach(() => { 129 | jest.useFakeTimers(); 130 | jest.clearAllMocks(); 131 | }); 132 | 133 | test('resolves with the file path if the video file is found and ready', async () => { 134 | jest 135 | .spyOn(attachmentUtils, 'getFilePathByGlobPattern') 136 | .mockImplementation(async () => 'path/to/video.mp4'); 137 | // .mockResolvedValueOnce('path/to/video.mp4'); 138 | jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockImplementation(async () => true); 139 | 140 | const promise = waitForVideoFile('*.mp4'); 141 | jest.runAllTimers(); 142 | 143 | await expect(promise).resolves.toBe('path/to/video.mp4'); 144 | }, 20000); 145 | 146 | test('retries until the video file is ready or timeout occurs', async () => { 147 | jest 148 | .spyOn(attachmentUtils, 'getFilePathByGlobPattern') 149 | .mockResolvedValueOnce('path/to/video.mp4'); 150 | jest 151 | .spyOn(attachmentUtils, 'checkVideoFileReady') 152 | .mockResolvedValueOnce(false) 153 | .mockResolvedValueOnce(false) 154 | .mockResolvedValueOnce(true); 155 | 156 | const promise = waitForVideoFile('*.mp4'); 157 | jest.advanceTimersByTime(3000); 158 | 159 | await expect(promise).resolves.toBe('path/to/video.mp4'); 160 | }, 20000); 161 | 162 | test('rejects with a timeout error if the timeout is reached without finding a ready video file', async () => { 163 | jest 164 | .spyOn(attachmentUtils, 'getFilePathByGlobPattern') 165 | .mockResolvedValueOnce('path/to/video.mp4'); 166 | jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockResolvedValueOnce(false); 167 | 168 | const promise = waitForVideoFile('*.mp4', 3000, 1000); 169 | jest.advanceTimersByTime(3000); 170 | 171 | await expect(promise).rejects.toThrow( 172 | 'Timeout of 3000ms reached, file *.mp4 not found or not ready yet.', 173 | ); 174 | }, 20000); 175 | 176 | afterEach(() => { 177 | jest.useRealTimers(); 178 | }); 179 | }); 180 | 181 | describe.skip('getVideoFile', () => { 182 | beforeEach(() => { 183 | jest.clearAllMocks(); 184 | }); 185 | 186 | test('returns the correct video file object if a valid video file is found and read successfully', async () => { 187 | const mockVideoFilePath = 'path/to/video.mp4'; 188 | const mockFileContent = 'base64encodedcontent'; 189 | jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath); 190 | jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileContent); 191 | 192 | const result = await getVideoFile('video', false, '**', 5000, 1000); 193 | 194 | expect(result).toEqual({ 195 | name: 'video.mp4', 196 | type: 'video/mp4', 197 | content: mockFileContent, 198 | }); 199 | }); 200 | 201 | test('returns null if no video file name is provided', async () => { 202 | const result = await getVideoFile(''); 203 | expect(result).toBeNull(); 204 | }); 205 | 206 | test('returns null and logs a warning if there is an error during the video file search', async () => { 207 | jest 208 | .spyOn(attachmentUtils, 'waitForVideoFile') 209 | .mockRejectedValueOnce(new Error('File not found')); 210 | jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); 211 | 212 | const result = await getVideoFile('video'); 213 | expect(result).toBeNull(); 214 | expect(console.warn).toHaveBeenCalledWith('File not found'); 215 | }); 216 | 217 | test('handles file read errors gracefully', async () => { 218 | const mockVideoFilePath = 'path/to/video.mp4'; 219 | jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath); 220 | jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file')); 221 | jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); 222 | 223 | const result = await getVideoFile('video'); 224 | expect(result).toBeNull(); 225 | expect(console.warn).toHaveBeenCalledWith('Failed to read file'); 226 | }); 227 | }); 228 | 229 | // TODO: add test for the real video file 230 | describe('compressVideo', () => { 231 | const spyPathJoin = jest.spyOn(path, 'join'); 232 | const spyDirnameJoin = jest.spyOn(path, 'dirname'); 233 | const spyBasenameJoin = jest.spyOn(path, 'basename'); 234 | 235 | const mockFilePath = 'path/to/video.mp4'; 236 | const mockOutputFilePath = 'path/to/compressed_video.mp4'; 237 | 238 | beforeEach(() => { 239 | spyPathJoin.mockReturnValueOnce(mockOutputFilePath); 240 | spyDirnameJoin.mockReturnValueOnce('path/to'); 241 | spyBasenameJoin.mockReturnValueOnce('video.mp4'); 242 | }); 243 | 244 | test('resolves with the correct output file path on successful compression', async () => { 245 | const mockFfmpeg = { 246 | outputOptions: jest.fn().mockReturnThis(), 247 | save: jest.fn().mockReturnThis(), 248 | on: jest.fn((event, handler) => { 249 | if (event === 'end') { 250 | handler(); 251 | } 252 | return mockFfmpeg; 253 | }), 254 | }; 255 | ffmpeg.mockReturnValue(mockFfmpeg); 256 | 257 | await expect(compressVideo(mockFilePath, 23)).resolves.toBe(mockOutputFilePath); 258 | expect(ffmpeg).toHaveBeenCalledWith(mockFilePath); 259 | expect(mockFfmpeg.outputOptions).toHaveBeenCalledWith('-crf 23'); 260 | expect(mockFfmpeg.save).toHaveBeenCalledWith(mockOutputFilePath); 261 | }); 262 | 263 | test('rejects with an error if compression fails', async () => { 264 | const mockError = new Error('Compression failed'); 265 | const mockFfmpeg = { 266 | outputOptions: jest.fn().mockReturnThis(), 267 | save: jest.fn().mockReturnThis(), 268 | on: jest.fn((event, handler) => { 269 | if (event === 'error') { 270 | handler(mockError); 271 | } 272 | return mockFfmpeg; 273 | }), 274 | }; 275 | ffmpeg.mockReturnValue(mockFfmpeg); 276 | 277 | await expect(compressVideo(mockFilePath, 23)).rejects.toThrow('Compression failed'); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /test/utils/common.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const { getCodeRef } = require('../../lib/utils/common'); 18 | 19 | describe('common utils', () => { 20 | describe('getCodeRef', () => { 21 | it('should return correct code ref for Windows paths', () => { 22 | jest.mock('path', () => ({ 23 | sep: '\\', 24 | })); 25 | const file = `test\\example.spec.js`; 26 | const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; 27 | 28 | const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; 29 | 30 | const codeRef = getCodeRef(titlePath, file); 31 | 32 | expect(codeRef).toEqual(expectedCodeRef); 33 | 34 | jest.clearAllMocks(); 35 | }); 36 | 37 | it('should return correct code ref for POSIX paths', () => { 38 | jest.mock('path', () => ({ 39 | sep: '/', 40 | })); 41 | const file = `test/example.spec.js`; 42 | const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; 43 | 44 | const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; 45 | 46 | const codeRef = getCodeRef(titlePath, file); 47 | 48 | expect(codeRef).toEqual(expectedCodeRef); 49 | 50 | jest.clearAllMocks(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/utils/objectCreators.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | const helpers = require('@reportportal/client-javascript/lib/helpers'); 19 | const { 20 | getSystemAttributes, 21 | getLaunchStartObject, 22 | getSuiteStartInfo, 23 | getSuiteEndInfo, 24 | getSuiteStartObject, 25 | getSuiteEndObject, 26 | getTestInfo, 27 | getTestStartObject, 28 | getTestEndObject, 29 | getHookInfo, 30 | getHookStartObject, 31 | getAgentInfo, 32 | getConfig, 33 | } = require('../../lib/utils/objectCreators'); 34 | const pjson = require('../../package.json'); 35 | 36 | const sep = path.sep; 37 | 38 | const { currentDate, getDefaultConfig } = require('../mock/mocks'); 39 | const { testItemStatuses, entityType } = require('../../lib/constants'); 40 | 41 | describe('object creators', () => { 42 | jest.spyOn(helpers, 'now').mockReturnValue(currentDate); 43 | 44 | const testFileName = `test${sep}example.spec.js`; 45 | 46 | describe('getAgentInfo', () => { 47 | it('getAgentInfo: should contain version and name properties', () => { 48 | const agentInfo = getAgentInfo(); 49 | 50 | expect(Object.keys(agentInfo)).toContain('version'); 51 | expect(Object.keys(agentInfo)).toContain('name'); 52 | }); 53 | }); 54 | 55 | describe('getSystemAttributes', () => { 56 | it('skippedIssue undefined. Should return attribute with agent name and version', function () { 57 | const options = getDefaultConfig(); 58 | const expectedSystemAttributes = [ 59 | { 60 | key: 'agent', 61 | value: `${pjson.name}|${pjson.version}`, 62 | system: true, 63 | }, 64 | ]; 65 | 66 | const systemAttributes = getSystemAttributes(options); 67 | 68 | expect(systemAttributes).toEqual(expectedSystemAttributes); 69 | }); 70 | 71 | it('skippedIssue = true. Should return attribute with agent name and version', function () { 72 | const options = getDefaultConfig(); 73 | options.reporterOptions.skippedIssue = true; 74 | const expectedSystemAttributes = [ 75 | { 76 | key: 'agent', 77 | value: `${pjson.name}|${pjson.version}`, 78 | system: true, 79 | }, 80 | ]; 81 | 82 | const systemAttributes = getSystemAttributes(options); 83 | 84 | expect(systemAttributes).toEqual(expectedSystemAttributes); 85 | }); 86 | 87 | it('skippedIssue = false. Should return 2 attribute: with agent name/version and skippedIssue', function () { 88 | const options = getDefaultConfig(); 89 | options.reporterOptions.skippedIssue = false; 90 | const expectedSystemAttributes = [ 91 | { 92 | key: 'agent', 93 | value: `${pjson.name}|${pjson.version}`, 94 | system: true, 95 | }, 96 | { 97 | key: 'skippedIssue', 98 | value: 'false', 99 | system: true, 100 | }, 101 | ]; 102 | 103 | const systemAttributes = getSystemAttributes(options); 104 | 105 | expect(systemAttributes).toEqual(expectedSystemAttributes); 106 | }); 107 | }); 108 | 109 | describe('getConfig', () => { 110 | const baseReporterOptions = { 111 | endpoint: 'https://reportportal.server/api/v1', 112 | project: 'ProjectName', 113 | launch: 'LauncherName', 114 | description: 'Launch description', 115 | attributes: [], 116 | }; 117 | 118 | describe('CI_BUILD_ID attribute providing', () => { 119 | afterEach(() => { 120 | delete process.env.CI_BUILD_ID; 121 | }); 122 | 123 | it('should not add an attribute with the CI_BUILD_ID value in case of parallel reporter option is false', function () { 124 | process.env.CI_BUILD_ID = 'buildId'; 125 | const initialConfig = { 126 | reporter: '@reportportal/agent-js-cypress', 127 | reporterOptions: { 128 | ...baseReporterOptions, 129 | apiKey: '123', 130 | autoMerge: true, 131 | parallel: false, 132 | }, 133 | }; 134 | const expectedConfig = initialConfig; 135 | 136 | const config = getConfig(initialConfig); 137 | 138 | expect(config).toEqual(expectedConfig); 139 | }); 140 | 141 | it('should not add an attribute with the CI_BUILD_ID value in case of autoMerge reporter option is false', function () { 142 | process.env.CI_BUILD_ID = 'buildId'; 143 | const initialConfig = { 144 | reporter: '@reportportal/agent-js-cypress', 145 | reporterOptions: { 146 | ...baseReporterOptions, 147 | apiKey: '123', 148 | autoMerge: false, 149 | parallel: true, 150 | }, 151 | }; 152 | const expectedConfig = initialConfig; 153 | 154 | const config = getConfig(initialConfig); 155 | 156 | expect(config).toEqual(expectedConfig); 157 | }); 158 | 159 | it('should not add an attribute with the value CI_BUILD_ID if the env variable CI_BUILD_ID does not exist', function () { 160 | process.env.CI_BUILD_ID = undefined; 161 | const initialConfig = { 162 | reporter: '@reportportal/agent-js-cypress', 163 | reporterOptions: { 164 | ...baseReporterOptions, 165 | apiKey: '123', 166 | autoMerge: false, 167 | parallel: true, 168 | }, 169 | }; 170 | const expectedConfig = initialConfig; 171 | 172 | const config = getConfig(initialConfig); 173 | 174 | expect(config).toEqual(expectedConfig); 175 | }); 176 | 177 | it('should return config with updated attributes (including attribute with CI_BUILD_ID value)', function () { 178 | process.env.CI_BUILD_ID = 'buildId'; 179 | const initialConfig = { 180 | reporter: '@reportportal/agent-js-cypress', 181 | reporterOptions: { 182 | ...baseReporterOptions, 183 | apiKey: '123', 184 | autoMerge: true, 185 | parallel: true, 186 | }, 187 | }; 188 | const expectedConfig = { 189 | reporter: '@reportportal/agent-js-cypress', 190 | reporterOptions: { 191 | ...initialConfig.reporterOptions, 192 | attributes: [ 193 | { 194 | value: 'buildId', 195 | }, 196 | ], 197 | }, 198 | }; 199 | 200 | const config = getConfig(initialConfig); 201 | 202 | expect(config).toEqual(expectedConfig); 203 | }); 204 | }); 205 | 206 | describe('apiKey option priority', () => { 207 | afterEach(() => { 208 | delete process.env.RP_TOKEN; 209 | delete process.env.RP_API_KEY; 210 | }); 211 | 212 | it('should override token property if the ENV variable RP_TOKEN exists', function () { 213 | process.env.RP_TOKEN = 'secret'; 214 | const initialConfig = { 215 | reporter: '@reportportal/agent-js-cypress', 216 | reporterOptions: { 217 | ...baseReporterOptions, 218 | token: '123', 219 | }, 220 | }; 221 | const expectedConfig = { 222 | reporter: '@reportportal/agent-js-cypress', 223 | reporterOptions: { 224 | ...baseReporterOptions, 225 | apiKey: 'secret', 226 | }, 227 | }; 228 | 229 | const config = getConfig(initialConfig); 230 | 231 | expect(config).toEqual(expectedConfig); 232 | }); 233 | 234 | it('should override apiKey property if the ENV variable RP_API_KEY exists', function () { 235 | process.env.RP_API_KEY = 'secret'; 236 | const initialConfig = { 237 | reporter: '@reportportal/agent-js-cypress', 238 | reporterOptions: { 239 | ...baseReporterOptions, 240 | apiKey: '123', 241 | }, 242 | }; 243 | const expectedConfig = { 244 | reporter: '@reportportal/agent-js-cypress', 245 | reporterOptions: { 246 | ...baseReporterOptions, 247 | apiKey: 'secret', 248 | }, 249 | }; 250 | 251 | const config = getConfig(initialConfig); 252 | 253 | expect(config).toEqual(expectedConfig); 254 | }); 255 | 256 | it('should prefer apiKey property over deprecated token', function () { 257 | const initialConfig = { 258 | reporter: '@reportportal/agent-js-cypress', 259 | reporterOptions: { 260 | ...baseReporterOptions, 261 | apiKey: '123', 262 | token: '345', 263 | }, 264 | }; 265 | const expectedConfig = { 266 | reporter: '@reportportal/agent-js-cypress', 267 | reporterOptions: { 268 | ...baseReporterOptions, 269 | apiKey: '123', 270 | }, 271 | }; 272 | 273 | const config = getConfig(initialConfig); 274 | 275 | expect(config).toEqual(expectedConfig); 276 | }); 277 | }); 278 | }); 279 | 280 | describe('getLaunchStartObject', () => { 281 | it('should return start launch object with correct values', () => { 282 | const expectedStartLaunchObject = { 283 | launch: 'LauncherName', 284 | description: 'Launch description', 285 | attributes: [ 286 | { 287 | key: 'agent', 288 | system: true, 289 | value: `${pjson.name}|${pjson.version}`, 290 | }, 291 | ], 292 | startTime: currentDate, 293 | rerun: undefined, 294 | rerunOf: undefined, 295 | mode: undefined, 296 | }; 297 | 298 | const startLaunchObject = getLaunchStartObject(getDefaultConfig()); 299 | 300 | expect(startLaunchObject).toBeDefined(); 301 | expect(startLaunchObject).toEqual(expectedStartLaunchObject); 302 | }); 303 | }); 304 | 305 | describe('getSuiteStartInfo', () => { 306 | it('root suite: should return suite start info with undefined parentId', () => { 307 | const suite = { 308 | id: 'suite1', 309 | title: 'suite name', 310 | description: 'suite description', 311 | root: true, 312 | titlePath: () => ['suite name'], 313 | }; 314 | const expectedSuiteStartInfo = { 315 | id: 'suite1', 316 | title: 'suite name', 317 | startTime: currentDate, 318 | description: 'suite description', 319 | codeRef: 'test/example.spec.js/suite name', 320 | parentId: undefined, 321 | testFileName: 'example.spec.js', 322 | }; 323 | 324 | const suiteStartInfo = getSuiteStartInfo(suite, testFileName); 325 | 326 | expect(suiteStartInfo).toBeDefined(); 327 | expect(suiteStartInfo).toEqual(expectedSuiteStartInfo); 328 | }); 329 | 330 | it('nested suite: should return suite start info with parentId', () => { 331 | const suite = { 332 | id: 'suite1', 333 | title: 'suite name', 334 | description: 'suite description', 335 | parent: { 336 | id: 'parentSuiteId', 337 | }, 338 | titlePath: () => ['parent suite name', 'suite name'], 339 | }; 340 | const expectedSuiteStartInfo = { 341 | id: 'suite1', 342 | title: 'suite name', 343 | startTime: currentDate, 344 | description: 'suite description', 345 | codeRef: 'test/example.spec.js/parent suite name/suite name', 346 | parentId: 'parentSuiteId', 347 | testFileName: 'example.spec.js', 348 | }; 349 | 350 | const suiteStartInfo = getSuiteStartInfo(suite, testFileName); 351 | 352 | expect(suiteStartInfo).toBeDefined(); 353 | expect(suiteStartInfo).toEqual(expectedSuiteStartInfo); 354 | }); 355 | }); 356 | 357 | describe('getSuiteEndInfo', () => { 358 | it('no tests inside suite: should return suite end info without status', () => { 359 | const suite = { 360 | id: 'suite1', 361 | title: 'suite name', 362 | description: 'suite description', 363 | parent: { 364 | id: 'parentSuiteId', 365 | }, 366 | }; 367 | const expectedSuiteEndInfo = { 368 | id: 'suite1', 369 | title: 'suite name', 370 | endTime: currentDate, 371 | }; 372 | 373 | const suiteEndInfo = getSuiteEndInfo(suite); 374 | 375 | expect(suiteEndInfo).toBeDefined(); 376 | expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); 377 | }); 378 | 379 | it('no failed tests inside suite: should return suite end info with undefined status', () => { 380 | const suite = { 381 | id: 'suite1', 382 | title: 'suite name', 383 | description: 'suite description', 384 | parent: { 385 | id: 'parentSuiteId', 386 | }, 387 | tests: [{ state: 'passed' }, { state: 'skipped' }], 388 | }; 389 | const expectedSuiteEndInfo = { 390 | id: 'suite1', 391 | title: 'suite name', 392 | endTime: currentDate, 393 | status: undefined, 394 | }; 395 | 396 | const suiteEndInfo = getSuiteEndInfo(suite); 397 | 398 | expect(suiteEndInfo).toBeDefined(); 399 | expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); 400 | }); 401 | 402 | it('there are failed tests inside suite: should return suite end info with failed status', () => { 403 | const suite = { 404 | id: 'suite1', 405 | title: 'suite name', 406 | description: 'suite description', 407 | parent: { 408 | id: 'parentSuiteId', 409 | }, 410 | tests: [{ state: 'failed' }, { state: 'passed' }], 411 | }; 412 | const expectedSuiteEndInfo = { 413 | id: 'suite1', 414 | title: 'suite name', 415 | endTime: currentDate, 416 | status: testItemStatuses.FAILED, 417 | }; 418 | 419 | const suiteEndInfo = getSuiteEndInfo(suite); 420 | 421 | expect(suiteEndInfo).toBeDefined(); 422 | expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); 423 | }); 424 | }); 425 | 426 | describe('getSuiteStartObject', () => { 427 | it('should return suite start object', () => { 428 | const suite = { 429 | id: 'suite1', 430 | title: 'suite name', 431 | startTime: currentDate, 432 | description: 'suite description', 433 | codeRef: 'test/example.spec.js/suite name', 434 | testFileName: 'example.spec.js', 435 | }; 436 | const expectedSuiteStartObject = { 437 | type: entityType.SUITE, 438 | name: 'suite name', 439 | startTime: currentDate, 440 | description: 'suite description', 441 | codeRef: 'test/example.spec.js/suite name', 442 | attributes: [], 443 | }; 444 | 445 | const suiteStartObject = getSuiteStartObject(suite); 446 | 447 | expect(suiteStartObject).toBeDefined(); 448 | expect(suiteStartObject).toEqual(expectedSuiteStartObject); 449 | }); 450 | }); 451 | 452 | describe('getSuiteEndObject', () => { 453 | it('should return suite end object', () => { 454 | const suite = { 455 | id: 'suite1', 456 | title: 'suite name', 457 | endTime: currentDate, 458 | status: testItemStatuses.FAILED, 459 | }; 460 | const expectedSuiteEndObject = { 461 | status: testItemStatuses.FAILED, 462 | endTime: currentDate, 463 | }; 464 | 465 | const suiteEndObject = getSuiteEndObject(suite); 466 | 467 | expect(suiteEndObject).toBeDefined(); 468 | expect(suiteEndObject).toEqual(expectedSuiteEndObject); 469 | }); 470 | }); 471 | 472 | describe('getTestInfo', () => { 473 | it('passed test: should return test info with passed status', () => { 474 | const test = { 475 | id: 'testId1', 476 | title: 'test name', 477 | parent: { 478 | id: 'parentSuiteId', 479 | }, 480 | state: 'passed', 481 | titlePath: () => ['suite name', 'test name'], 482 | }; 483 | const expectedTestInfoObject = { 484 | id: 'testId1', 485 | title: 'test name', 486 | status: 'passed', 487 | parentId: 'parentSuiteId', 488 | codeRef: 'test/example.spec.js/suite name/test name', 489 | err: undefined, 490 | testFileName, 491 | }; 492 | 493 | const testInfoObject = getTestInfo(test, testFileName); 494 | 495 | expect(testInfoObject).toBeDefined(); 496 | expect(testInfoObject).toEqual(expectedTestInfoObject); 497 | }); 498 | 499 | it('pending test: should return test info with skipped status', () => { 500 | const test = { 501 | id: 'testId1', 502 | title: 'test name', 503 | parent: { 504 | id: 'parentSuiteId', 505 | }, 506 | state: 'pending', 507 | titlePath: () => ['suite name', 'test name'], 508 | }; 509 | const expectedTestInfoObject = { 510 | id: 'testId1', 511 | title: 'test name', 512 | status: 'skipped', 513 | parentId: 'parentSuiteId', 514 | codeRef: 'test/example.spec.js/suite name/test name', 515 | err: undefined, 516 | testFileName, 517 | }; 518 | 519 | const testInfoObject = getTestInfo(test, testFileName); 520 | 521 | expect(testInfoObject).toBeDefined(); 522 | expect(testInfoObject).toEqual(expectedTestInfoObject); 523 | }); 524 | 525 | it('should return test info with specified status and error', () => { 526 | const test = { 527 | id: 'testId', 528 | title: 'test name', 529 | parent: { 530 | id: 'parentSuiteId', 531 | }, 532 | state: 'pending', 533 | titlePath: () => ['suite name', 'test name'], 534 | }; 535 | const expectedTestInfoObject = { 536 | id: 'testId', 537 | title: 'test name', 538 | status: 'failed', 539 | parentId: 'parentSuiteId', 540 | codeRef: 'test/example.spec.js/suite name/test name', 541 | err: { message: 'error message' }, 542 | testFileName, 543 | }; 544 | 545 | const testInfoObject = getTestInfo(test, testFileName, 'failed', { 546 | message: 'error message', 547 | }); 548 | 549 | expect(testInfoObject).toBeDefined(); 550 | expect(testInfoObject).toEqual(expectedTestInfoObject); 551 | }); 552 | }); 553 | 554 | describe('getTestStartObject', () => { 555 | it('should return test start object', () => { 556 | const test = { 557 | id: 'testId1', 558 | title: 'test name', 559 | parent: { 560 | id: 'parentSuiteId', 561 | }, 562 | codeRef: 'test/example.spec.js/suite name/test name', 563 | }; 564 | const expectedTestStartObject = { 565 | name: 'test name', 566 | startTime: currentDate, 567 | attributes: [], 568 | type: 'step', 569 | codeRef: 'test/example.spec.js/suite name/test name', 570 | }; 571 | 572 | const testInfoObject = getTestStartObject(test); 573 | 574 | expect(testInfoObject).toBeDefined(); 575 | expect(testInfoObject).toEqual(expectedTestStartObject); 576 | }); 577 | }); 578 | 579 | describe('getTestEndObject', () => { 580 | it('skippedIssue is not defined: should return test end object without issue', () => { 581 | const testInfo = { 582 | id: 'testId1', 583 | title: 'test name', 584 | status: 'skipped', 585 | parent: { 586 | id: 'parentSuiteId', 587 | }, 588 | }; 589 | const expectedTestEndObject = { 590 | endTime: currentDate, 591 | status: testInfo.status, 592 | }; 593 | const testEndObject = getTestEndObject(testInfo); 594 | 595 | expect(testEndObject).toBeDefined(); 596 | expect(testEndObject).toEqual(expectedTestEndObject); 597 | }); 598 | 599 | it('skippedIssue = true: should return test end object without issue', () => { 600 | const testInfo = { 601 | id: 'testId1', 602 | title: 'test name', 603 | status: 'skipped', 604 | parent: { 605 | id: 'parentSuiteId', 606 | }, 607 | }; 608 | const expectedTestEndObject = { 609 | endTime: currentDate, 610 | status: testInfo.status, 611 | }; 612 | const testEndObject = getTestEndObject(testInfo, true); 613 | 614 | expect(testEndObject).toBeDefined(); 615 | expect(testEndObject).toEqual(expectedTestEndObject); 616 | }); 617 | 618 | it('skippedIssue = false: should return test end object with issue NOT_ISSUE', () => { 619 | const testInfo = { 620 | id: 'testId1', 621 | title: 'test name', 622 | status: 'skipped', 623 | parent: { 624 | id: 'parentSuiteId', 625 | }, 626 | }; 627 | const expectedTestEndObject = { 628 | endTime: currentDate, 629 | status: testInfo.status, 630 | issue: { 631 | issueType: 'NOT_ISSUE', 632 | }, 633 | }; 634 | const testEndObject = getTestEndObject(testInfo, false); 635 | 636 | expect(testEndObject).toBeDefined(); 637 | expect(testEndObject).toEqual(expectedTestEndObject); 638 | }); 639 | 640 | it('testCaseId is defined: should return test end object with testCaseId', () => { 641 | const testInfo = { 642 | id: 'testId1', 643 | title: 'test name', 644 | status: 'skipped', 645 | parent: { 646 | id: 'parentSuiteId', 647 | }, 648 | testCaseId: 'testCaseId', 649 | }; 650 | const expectedTestEndObject = { 651 | endTime: currentDate, 652 | status: testInfo.status, 653 | testCaseId: 'testCaseId', 654 | }; 655 | const testEndObject = getTestEndObject(testInfo); 656 | 657 | expect(testEndObject).toEqual(expectedTestEndObject); 658 | }); 659 | }); 660 | 661 | describe('getHookInfo', () => { 662 | it('passed before each hook: should return hook info with passed status', () => { 663 | const hook = { 664 | id: 'testId', 665 | title: '"before each" hook: hook name', 666 | parent: { 667 | id: 'parentSuiteId', 668 | }, 669 | state: 'passed', 670 | hookName: 'before each', 671 | hookId: 'hookId', 672 | titlePath: () => ['suite name', 'hook name'], 673 | }; 674 | const expectedHookInfoObject = { 675 | id: 'hookId_testId', 676 | hookName: 'before each', 677 | title: '"before each" hook: hook name', 678 | status: 'passed', 679 | parentId: 'parentSuiteId', 680 | codeRef: 'test/example.spec.js/suite name/hook name', 681 | err: undefined, 682 | testFileName, 683 | }; 684 | 685 | const hookInfoObject = getHookInfo(hook, testFileName); 686 | 687 | expect(hookInfoObject).toBeDefined(); 688 | expect(hookInfoObject).toEqual(expectedHookInfoObject); 689 | }); 690 | 691 | it('passed before all hook: should return correct hook info', () => { 692 | const hook = { 693 | id: 'testId', 694 | title: '"before all" hook: hook name', 695 | parent: { 696 | id: 'parentSuiteId', 697 | title: 'parent suite title', 698 | parent: { 699 | id: 'rootSuiteId', 700 | title: 'root suite title', 701 | }, 702 | }, 703 | state: 'passed', 704 | hookName: 'before all', 705 | hookId: 'hookId', 706 | titlePath: () => ['suite name', 'hook name'], 707 | }; 708 | const expectedHookInfoObject = { 709 | id: 'hookId_testId', 710 | hookName: 'before all', 711 | title: '"before all" hook: hook name', 712 | status: 'passed', 713 | parentId: 'rootSuiteId', 714 | codeRef: 'test/example.spec.js/suite name/hook name', 715 | err: undefined, 716 | testFileName, 717 | }; 718 | 719 | const hookInfoObject = getHookInfo(hook, testFileName); 720 | 721 | expect(hookInfoObject).toBeDefined(); 722 | expect(hookInfoObject).toEqual(expectedHookInfoObject); 723 | }); 724 | 725 | it('failed test: should return hook info with failed status', () => { 726 | const test = { 727 | id: 'testId', 728 | hookName: 'before each', 729 | title: '"before each" hook: hook name', 730 | parent: { 731 | id: 'parentSuiteId', 732 | }, 733 | state: 'failed', 734 | failedFromHookId: 'hookId', 735 | titlePath: () => ['suite name', 'hook name'], 736 | }; 737 | const expectedHookInfoObject = { 738 | id: 'hookId_testId', 739 | hookName: 'before each', 740 | title: '"before each" hook: hook name', 741 | status: 'failed', 742 | parentId: 'parentSuiteId', 743 | codeRef: 'test/example.spec.js/suite name/hook name', 744 | err: undefined, 745 | testFileName, 746 | }; 747 | 748 | const hookInfoObject = getHookInfo(test, testFileName); 749 | 750 | expect(hookInfoObject).toBeDefined(); 751 | expect(hookInfoObject).toEqual(expectedHookInfoObject); 752 | }); 753 | }); 754 | 755 | describe('getHookStartObject', () => { 756 | it('should return hook start object', () => { 757 | const hookInfo = { 758 | id: 'hookId_testId', 759 | hookName: 'before each', 760 | title: '"before each" hook: hook name', 761 | status: 'passed', 762 | parentId: 'parentSuiteId', 763 | titlePath: () => ['suite name', 'hook name'], 764 | err: undefined, 765 | }; 766 | const expectedHookStartObject = { 767 | name: 'hook name', 768 | startTime: currentDate, 769 | type: 'BEFORE_METHOD', 770 | }; 771 | 772 | const hookInfoObject = getHookStartObject(hookInfo, testFileName, 'failed', { 773 | message: 'error message', 774 | }); 775 | 776 | expect(hookInfoObject).toBeDefined(); 777 | expect(hookInfoObject).toEqual(expectedHookStartObject); 778 | }); 779 | }); 780 | }); 781 | -------------------------------------------------------------------------------- /test/utils/specCountCalculation.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 EPAM Systems 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const mock = require('mock-fs'); 18 | const path = require('path'); 19 | const { 20 | getTotalSpecs, 21 | getFixtureFolderPattern, 22 | getExcludeSpecPattern, 23 | getSpecPattern, 24 | } = require('../../lib/utils/specCountCalculation'); 25 | 26 | const sep = path.sep; 27 | 28 | describe('spec count calculation', () => { 29 | describe('getTotalSpecs', () => { 30 | beforeEach(() => { 31 | mock({ 32 | 'cypress/tests': { 33 | 'example1.spec.js': '', 34 | 'example2.spec.js': '', 35 | 'example3.spec.js': '', 36 | 'example4.spec.ts': '', 37 | 'example.ignore.spec.js': '', 38 | }, 39 | 'cypress/support': { 40 | 'index.js': '', 41 | }, 42 | 'cypress/fixtures': { 43 | 'fixtures1.js': '', 44 | 'fixtures2.js': '', 45 | }, 46 | }); 47 | }); 48 | 49 | afterEach(() => { 50 | mock.restore(); 51 | }); 52 | 53 | it('testFiles, integrationFolder, supportFile are specified: should count all files from integration folder', () => { 54 | let specConfig = { 55 | testFiles: '**/*.*', 56 | ignoreTestFiles: '*.hot-update.js', 57 | fixturesFolder: 'cypress/fixtures', 58 | integrationFolder: 'cypress/tests', 59 | supportFile: 'cypress/support/index.js', 60 | }; 61 | 62 | let specCount = getTotalSpecs(specConfig); 63 | 64 | expect(specCount).toEqual(5); 65 | 66 | specConfig = { 67 | excludeSpecPattern: '*.hot-update.js', 68 | specPattern: 'cypress/tests/**/*.spec.{js,ts}', 69 | supportFile: 'cypress/support/index.js', 70 | fixturesFolder: 'cypress/fixtures', 71 | }; 72 | 73 | specCount = getTotalSpecs(specConfig); 74 | 75 | expect(specCount).toEqual(5); 76 | }); 77 | 78 | it('nor testFiles nor specPattern are specified: should throw an exception', () => { 79 | expect(() => { 80 | getTotalSpecs({}); 81 | }).toThrow( 82 | new Error('Configuration property not set! Neither for cypress <= 9 nor cypress >= 10'), 83 | ); 84 | }); 85 | 86 | it('ignoreTestFiles are specified: should ignore specified files', () => { 87 | let specConfig = { 88 | testFiles: '**/*.*', 89 | ignoreTestFiles: ['*.hot-update.js', '*.ignore.*.*'], 90 | fixturesFolder: 'cypress/fixtures', 91 | integrationFolder: 'cypress/tests', 92 | supportFile: 'cypress/support/index.js', 93 | }; 94 | 95 | let specCount = getTotalSpecs(specConfig); 96 | 97 | expect(specCount).toEqual(4); 98 | 99 | specConfig = { 100 | specPattern: 'cypress/tests/**/*.spec.{js,ts}', 101 | excludeSpecPattern: ['*.hot-update.js', '*.ignore.spec.*'], 102 | supportFile: 'cypress/support/index.js', 103 | fixturesFolder: 'cypress/fixtures', 104 | }; 105 | 106 | specCount = getTotalSpecs(specConfig); 107 | 108 | expect(specCount).toEqual(4); 109 | }); 110 | }); 111 | 112 | describe('getFixtureFolderPattern', () => { 113 | it('returns a glob pattern for fixtures folder', () => { 114 | const specConfig = { fixturesFolder: `cypress${sep}fixtures` }; 115 | 116 | const specArray = getFixtureFolderPattern(specConfig); 117 | expect(specArray).toHaveLength(1); 118 | expect(specArray).toContain(`cypress${sep}fixtures${sep}**${sep}*`); 119 | }); 120 | }); 121 | 122 | describe('getExcludeSpecPattern', () => { 123 | it('getExcludeSpecPattern returns required pattern for cypress version >= 10', () => { 124 | const specConfigString = { 125 | excludeSpecPattern: '*.hot-update.js', 126 | }; 127 | 128 | const specConfigArray = { 129 | excludeSpecPattern: ['*.hot-update.js', '*.hot-update.ts'], 130 | }; 131 | 132 | let patternArray = getExcludeSpecPattern(specConfigString); 133 | expect(patternArray).toHaveLength(1); 134 | expect(patternArray).toContain('*.hot-update.js'); 135 | 136 | patternArray = getExcludeSpecPattern(specConfigArray); 137 | expect(patternArray).toHaveLength(2); 138 | expect(patternArray).toContain('*.hot-update.js'); 139 | expect(patternArray).toContain('*.hot-update.ts'); 140 | }); 141 | it('getExcludeSpecPattern returns required pattern for cypress version <= 9', () => { 142 | const specConfigString = { 143 | integrationFolder: 'cypress/integration', 144 | ignoreTestFiles: '*.hot-update.js', 145 | fixturesFolder: 'cypress/fixtures', 146 | supportFile: 'cypress/support/index.js', 147 | }; 148 | 149 | const specConfigArray = { 150 | integrationFolder: 'cypress/integration', 151 | ignoreTestFiles: ['*.hot-update.js', '*.hot-update.ts'], 152 | fixturesFolder: 'cypress/fixtures', 153 | supportFile: 'cypress/support/index.js', 154 | }; 155 | 156 | let patternArray = getExcludeSpecPattern(specConfigString); 157 | expect(patternArray).toHaveLength(1); 158 | expect(patternArray).toContain('*.hot-update.js'); 159 | 160 | patternArray = getExcludeSpecPattern(specConfigArray); 161 | expect(patternArray).toHaveLength(2); 162 | expect(patternArray).toContain('*.hot-update.js'); 163 | expect(patternArray).toContain('*.hot-update.ts'); 164 | }); 165 | }); 166 | 167 | describe('getSpecPattern', () => { 168 | it('returns the required glob pattern for cypress <=9 config when testFiles is an array', () => { 169 | const specConfig = { 170 | integrationFolder: 'cypress/integration', 171 | testFiles: ['**/*.js', '**/*.ts'], 172 | }; 173 | 174 | const patternArray = getSpecPattern(specConfig); 175 | expect(patternArray).toHaveLength(2); 176 | expect(patternArray[0]).toEqual('cypress/integration/**/*.js'); 177 | expect(patternArray[1]).toEqual('cypress/integration/**/*.ts'); 178 | }); 179 | 180 | it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is an array', () => { 181 | const specConfig = { 182 | specPattern: ['cypress/integration/**/*.js', 'cypress/integration/**/*.js'], 183 | }; 184 | 185 | const patternArray = getSpecPattern(specConfig); 186 | expect(patternArray).toHaveLength(2); 187 | expect(patternArray[0]).toEqual(specConfig.specPattern[0]); 188 | expect(patternArray[1]).toEqual(specConfig.specPattern[1]); 189 | }); 190 | 191 | it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is a string', () => { 192 | const specConfig = { 193 | specPattern: 'cypress/integration/**/*.js', 194 | }; 195 | 196 | const patternArray = getSpecPattern(specConfig); 197 | expect(patternArray).toHaveLength(1); 198 | expect(patternArray[0]).toEqual(specConfig.specPattern); 199 | }); 200 | 201 | it('getSpecPattern returns the required glob pattern for cypress <= 9 config when testFiles is a string', () => { 202 | const specConfig = { 203 | integrationFolder: 'cypress/integration', 204 | testFiles: '**/*.js', 205 | }; 206 | 207 | const patternArray = getSpecPattern(specConfig); 208 | expect(patternArray).toHaveLength(1); 209 | expect(patternArray[0]).toEqual('cypress/integration/**/*.js'); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /version_fragment: -------------------------------------------------------------------------------- 1 | patch 2 | --------------------------------------------------------------------------------