├── .github └── workflows │ ├── assets.yml │ ├── build.yml │ ├── pr.yaml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets ├── chat.png ├── cucumber.png ├── hyperlink.png ├── jmeter.png ├── junit.png ├── logo.png ├── mentions.png ├── mocha.png ├── quickchart.png ├── reportportal.jpeg ├── slack-report-portal-analysis.png ├── slack.png ├── teams-qc.png ├── teams.png ├── testbeats-failure-summary.png ├── testbeats-slack-failure-summary.png ├── testng.png └── xunit.png ├── mocha.report.json ├── package-lock.json ├── package.json ├── scripts ├── download-latest.sh └── download.sh ├── src ├── beats │ ├── beats.api.js │ ├── beats.attachments.js │ ├── beats.js │ ├── beats.types.d.ts │ └── index.js ├── cli.js ├── commands │ ├── generate-config.command.js │ └── publish.command.js ├── extensions │ ├── ai-failure-summary.extension.js │ ├── base.extension.js │ ├── browserstack.extension.js │ ├── ci-info.extension.js │ ├── custom.extension.js │ ├── error-clusters.extension.js │ ├── extensions.d.ts │ ├── failure-analysis.extension.js │ ├── hyperlinks.js │ ├── index.js │ ├── mentions.js │ ├── metadata.js │ ├── percy-analysis.js │ ├── quick-chart-test-summary.js │ ├── report-portal-analysis.js │ ├── report-portal-history.js │ └── smart-analysis.extension.js ├── helpers │ ├── browserstack.helper.js │ ├── ci │ │ ├── azure-devops.js │ │ ├── base.ci.js │ │ ├── circle-ci.js │ │ ├── github.js │ │ ├── gitlab.js │ │ ├── index.js │ │ └── jenkins.js │ ├── constants.js │ ├── extension.helper.js │ ├── helper.js │ ├── metadata.helper.js │ ├── percy.js │ ├── performance.js │ └── report-portal.js ├── index.d.ts ├── index.js ├── platforms │ ├── base.platform.js │ ├── chat.platform.js │ ├── index.js │ ├── slack.platform.js │ └── teams.platform.js ├── setups │ └── extensions.setup.js ├── targets │ ├── chat.js │ ├── custom.js │ ├── delay.js │ ├── index.js │ ├── influx.js │ ├── slack.js │ └── teams.js └── utils │ ├── config.builder.js │ └── logger.js └── test ├── base.spec.js ├── beats.spec.js ├── ci.spec.js ├── cli.spec.js ├── condition.spec.js ├── config.spec.js ├── data ├── configs │ ├── custom-target.json │ ├── slack.config.json │ └── teams.config.json ├── cucumber │ ├── failed-tests-with-attachments.json │ └── suites-with-metadata.json ├── custom │ ├── custom-runner.js │ └── custom-target-runner.js ├── jmeter │ └── sample.csv ├── junit │ └── single-suite.xml ├── playwright │ ├── example-get-started-link-chromium │ │ └── test-failed-1.png │ ├── example-get-started-link-firefox │ │ └── test-failed-1.png │ └── junit.xml └── testng │ ├── multiple-suites-failures.xml │ ├── multiple-suites-retries.xml │ ├── multiple-suites.xml │ ├── single-suite-failures.xml │ └── single-suite.xml ├── ext-ci-info.spec.js ├── ext-global.spec.js ├── ext.browserstack.spec.js ├── ext.custom.spec.js ├── ext.hyperlink.spec.js ├── ext.mentions.spec.js ├── ext.metadata.spec.js ├── ext.percy-analysis.spec.js ├── ext.quick-chart-test-summary.spec.js ├── ext.report-portal-analysis.spec.js ├── ext.report-portal-history.spec.js ├── handle-errors.spec.js ├── helpers └── interactions.js ├── mocks ├── beats.mock.js ├── browserstack.mock.js ├── chat.mock.js ├── custom.mock.js ├── index.js ├── influx.mock.js ├── influx2.mock.js ├── percy.mock.js ├── rp.mock.js ├── slack.mock.js └── teams.mock.js ├── publish.spec.js ├── results.custom.spec.js ├── results.junit.spec.js ├── results.testng.spec.js ├── results.xunit.spec.js ├── targets.chat.spec.js ├── targets.custom.spec.js ├── targets.delay.spec.js ├── targets.influx.spec.js ├── targets.influx2.spec.js ├── targets.slack.spec.js ├── targets.spec.js └── targets.teams.spec.js /.github/workflows/assets.yml: -------------------------------------------------------------------------------- 1 | name: Upload Assets 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: '18.x' 15 | - run: npm install 16 | - run: npm run build 17 | - uses: vimtor/action-zip@v1 18 | with: 19 | files: dist/testbeats-linux 20 | dest: dist/testbeats-linux.zip 21 | - uses: vimtor/action-zip@v1 22 | with: 23 | files: dist/testbeats-macos 24 | dest: dist/testbeats-macos.zip 25 | - uses: vimtor/action-zip@v1 26 | with: 27 | files: dist/testbeats-win.exe 28 | dest: dist/testbeats-win.exe.zip 29 | - name: Upload release binaries 30 | uses: alexellis/upload-assets@0.2.2 31 | env: 32 | GITHUB_TOKEN: ${{ github.token }} 33 | with: 34 | asset_paths: '["./dist/*zip"]' 35 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node-version: [20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm run test 28 | env: 29 | CI: true 30 | - run: node src/cli.js publish --slack '{SLACK_MVP_URL}' --title 'Unit Tests' --ci-info --chart-test-summary --junit 'results/junit.xml' 31 | if: always() 32 | env: 33 | TEST_BEATS_API_KEY: ${{ secrets.TEST_BEATS_API_KEY }} 34 | SLACK_MVP_URL: ${{ secrets.SLACK_MVP_URL }} 35 | - uses: actions/upload-artifact@v4 36 | if: always() 37 | with: 38 | name: results-${{ matrix.os }}-${{ matrix.node-version }} 39 | path: results/ -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'README.md' 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [windows-latest, ubuntu-latest, macos-latest] 18 | node-version: [20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm run test 28 | env: 29 | CI: true -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '18.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm install 16 | - run: npm test 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IntelliJ IDEA 107 | .idea 108 | 109 | .DS_STORE 110 | 111 | # test 112 | results 113 | 114 | 115 | # ignore test/override config files 116 | override-config*.json 117 | 118 | .testbeats 119 | .testbeats.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "analysed", 4 | "mstest", 5 | "nunit", 6 | "pactum", 7 | "testbeats", 8 | "testng", 9 | "xunit" 10 | ] 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Anudeep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This npm package has been renamed from [test-results-reporter](https://www.npmjs.com/package/test-results-reporter) to [testbeats](https://www.npmjs.com/package/testbeats). test-results-reporter will soon be phased out, and users are encouraged to transition to testbeats. 2 | 3 | 4 | 5 | ![logo](https://github.com/test-results-reporter/testbeats/raw/main/assets/logo.png) 6 | 7 | 8 | #### Publish test results to Microsoft Teams, Google Chat, Slack and many more. 9 | 10 |
11 | 12 | [![Build](https://github.com/test-results-reporter/testbeats/actions/workflows/build.yml/badge.svg)](https://github.com/test-results-reporter/testbeats/actions/workflows/build.yml) 13 | [![Downloads](https://img.shields.io/npm/dt/test-results-parser?logo=npm&label=downloads)](https://www.npmjs.com/package/test-results-parser) 14 | [![Size](https://img.shields.io/bundlephobia/minzip/testbeats)](https://bundlephobia.com/result?p=testbeats) 15 | 16 | 17 | [![Stars](https://img.shields.io/github/stars/test-results-reporter/testbeats?style=social)](https://github.com/test-results-reporter/testbeats/stargazers) 18 | ![Downloads](https://img.shields.io/github/downloads/test-results-reporter/testbeats/total?logo=github) 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | **TestBeats** is a tool designed to streamline the process of publishing test results from various automation testing frameworks to communication platforms like **slack**, **teams** and more for easy access and collaboration. It unifies your test reporting to build quality insights and make faster decisions. 27 | 28 | It supports all major automation testing frameworks and tools. 29 | 30 | Read more about the project at [https://testbeats.com](https://testbeats.com) 31 | 32 | ### Sample Reports 33 | 34 | #### Alerts in Slack 35 | 36 | ![testbeats-failure-summary](./assets/testbeats-slack-failure-summary.png) 37 | 38 | #### Results in Portal 39 | 40 | ![testbeats-failure-summary](./assets/testbeats-failure-summary.png) 41 | 42 |
43 | 44 | ## Need Help 45 | 46 | We use [Github Discussions](https://github.com/test-results-reporter/testbeats/discussions) to receive feedback, discuss ideas & answer questions. Head over to it and feel free to start a discussion. We are always happy to help 😊. 47 | 48 | ## Support Us 49 | 50 | Like this project! Star it on [Github](https://github.com/test-results-reporter/testbeats) ⭐. Your support means a lot to us. 51 | 52 |
53 | 54 | > Read more about the project at [https://testbeats.com](https://testbeats.com) -------------------------------------------------------------------------------- /assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/chat.png -------------------------------------------------------------------------------- /assets/cucumber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/cucumber.png -------------------------------------------------------------------------------- /assets/hyperlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/hyperlink.png -------------------------------------------------------------------------------- /assets/jmeter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/jmeter.png -------------------------------------------------------------------------------- /assets/junit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/junit.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/logo.png -------------------------------------------------------------------------------- /assets/mentions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/mentions.png -------------------------------------------------------------------------------- /assets/mocha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/mocha.png -------------------------------------------------------------------------------- /assets/quickchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/quickchart.png -------------------------------------------------------------------------------- /assets/reportportal.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/reportportal.jpeg -------------------------------------------------------------------------------- /assets/slack-report-portal-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/slack-report-portal-analysis.png -------------------------------------------------------------------------------- /assets/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/slack.png -------------------------------------------------------------------------------- /assets/teams-qc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/teams-qc.png -------------------------------------------------------------------------------- /assets/teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/teams.png -------------------------------------------------------------------------------- /assets/testbeats-failure-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/testbeats-failure-summary.png -------------------------------------------------------------------------------- /assets/testbeats-slack-failure-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/testbeats-slack-failure-summary.png -------------------------------------------------------------------------------- /assets/testng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/testng.png -------------------------------------------------------------------------------- /assets/xunit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/assets/xunit.png -------------------------------------------------------------------------------- /mocha.report.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "results/junit.xml" 5 | } 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testbeats", 3 | "version": "2.2.2", 4 | "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB", 5 | "main": "src/index.js", 6 | "types": "./src/index.d.ts", 7 | "bin": { 8 | "testbeats": "src/cli.js" 9 | }, 10 | "files": [ 11 | "/src" 12 | ], 13 | "scripts": { 14 | "test": "c8 mocha test --reporter mocha-multi-reporters --reporter-options configFile=mocha.report.json", 15 | "build": "pkg --out-path dist ." 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/test-results-reporter/testbeats.git" 20 | }, 21 | "keywords": [ 22 | "test", 23 | "results", 24 | "publish", 25 | "report", 26 | "microsoft teams", 27 | "teams", 28 | "slack", 29 | "influx", 30 | "influxdb", 31 | "junit", 32 | "mocha", 33 | "cucumber", 34 | "testng", 35 | "xunit", 36 | "reportportal", 37 | "google chat", 38 | "chat", 39 | "percy", 40 | "jmeter" 41 | ], 42 | "author": "", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/test-results-reporter/testbeats/issues" 46 | }, 47 | "homepage": "https://testbeats.com", 48 | "dependencies": { 49 | "async-retry": "^1.3.3", 50 | "dotenv": "^16.4.5", 51 | "form-data-lite": "^1.0.3", 52 | "influxdb-lite": "^1.0.0", 53 | "performance-results-parser": "latest", 54 | "phin-retry": "^1.0.3", 55 | "pretty-ms": "^7.0.1", 56 | "prompts": "^2.4.2", 57 | "rosters": "0.0.1", 58 | "sade": "^1.8.1", 59 | "test-results-parser": "0.2.6" 60 | }, 61 | "devDependencies": { 62 | "c8": "^10.1.2", 63 | "mocha": "^10.7.3", 64 | "mocha-junit-reporter": "^2.2.1", 65 | "mocha-multi-reporters": "^1.5.1", 66 | "pactum": "^3.7.1", 67 | "pkg": "^5.8.1" 68 | }, 69 | "engines": { 70 | "node": ">=14.0.0" 71 | }, 72 | "engineStrict": true 73 | } 74 | -------------------------------------------------------------------------------- /scripts/download-latest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | case "$OSTYPE" in 4 | darwin*) file_name=testbeats-macos ;; 5 | linux*) file_name=testbeats-linux ;; 6 | msys*) file_name=testbeats-win.exe ;; 7 | cygwin*) file_name=testbeats-win.exe ;; 8 | *) echo "$OSTYPE OS - Not Supported"; exit 1 ;; 9 | esac 10 | 11 | latest_tag=$(curl -s https://api.github.com/repos/test-results-reporter/testbeats/releases/latest | sed -Ene '/^ *"tag_name": *"(v.+)",$/s//\1/p') 12 | curl -JLO https://github.com/test-results-reporter/testbeats/releases/download/${latest_tag}/${file_name}.zip 13 | unzip ${file_name}.zip 14 | chmod +x ${file_name} -------------------------------------------------------------------------------- /scripts/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | case "$OSTYPE" in 4 | darwin*) file_name=test-results-reporter-macos ;; 5 | linux*) file_name=test-results-reporter-linux ;; 6 | msys*) file_name=test-results-reporter-win.exe ;; 7 | cygwin*) file_name=test-results-reporter-win.exe ;; 8 | *) echo "$OSTYPE OS - Not Supported"; exit 1 ;; 9 | esac 10 | 11 | latest_tag=$(curl -s https://api.github.com/repos/test-results-reporter/testbeats/releases/latest | sed -Ene '/^ *"tag_name": *"(v.+)",$/s//\1/p') 12 | curl -JLO https://github.com/test-results-reporter/testbeats/releases/download/v1.1.6/${file_name}.zip 13 | unzip ${file_name}.zip 14 | chmod +x ${file_name} -------------------------------------------------------------------------------- /src/beats/beats.api.js: -------------------------------------------------------------------------------- 1 | const request = require('phin-retry'); 2 | 3 | class BeatsApi { 4 | 5 | /** 6 | * @param {import('../index').PublishReport} config 7 | */ 8 | constructor(config) { 9 | this.config = config; 10 | } 11 | 12 | postTestRun(payload) { 13 | return request.post({ 14 | url: `${this.getBaseUrl()}/api/core/v1/test-runs`, 15 | headers: { 16 | 'x-api-key': this.config.api_key 17 | }, 18 | body: payload 19 | }); 20 | } 21 | 22 | /** 23 | * @param {string} run_id 24 | * @returns 25 | */ 26 | getTestRun(run_id) { 27 | return request.get({ 28 | url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}`, 29 | headers: { 30 | 'x-api-key': this.config.api_key 31 | } 32 | }); 33 | } 34 | 35 | uploadAttachments(headers, payload) { 36 | return request.post({ 37 | url: `${this.getBaseUrl()}/api/core/v1/test-cases/attachments`, 38 | headers: { 39 | 'x-api-key': this.config.api_key, 40 | ...headers 41 | }, 42 | body: payload 43 | }); 44 | } 45 | 46 | getBaseUrl() { 47 | return process.env.TEST_BEATS_URL || "https://app.testbeats.com"; 48 | } 49 | 50 | /** 51 | * 52 | * @param {string} run_id 53 | * @param {number} limit 54 | * @returns {import('./beats.types').IErrorClustersResponse} 55 | */ 56 | getErrorClusters(run_id, limit = 3) { 57 | return request.get({ 58 | url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/error-clusters?limit=${limit}`, 59 | headers: { 60 | 'x-api-key': this.config.api_key 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * 67 | * @param {string} run_id 68 | * @returns {import('./beats.types').IFailureAnalysisMetric[]} 69 | */ 70 | getFailureAnalysis(run_id) { 71 | return request.get({ 72 | url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/failure-analysis`, 73 | headers: { 74 | 'x-api-key': this.config.api_key 75 | } 76 | }); 77 | } 78 | } 79 | 80 | module.exports = { BeatsApi } -------------------------------------------------------------------------------- /src/beats/beats.types.d.ts: -------------------------------------------------------------------------------- 1 | export type IBeatExecutionMetric = { 2 | id: string 3 | created_at: string 4 | updated_at: string 5 | newly_failed: number 6 | always_failing: number 7 | recurring_failures: number 8 | recovered: number 9 | added: number 10 | removed: number 11 | flaky: number 12 | failure_summary: any 13 | failure_summary_provider: any 14 | failure_summary_model: any 15 | status: string 16 | status_message: any 17 | test_run_id: string 18 | org_id: string 19 | } 20 | 21 | export type IPaginatedAPIResponse = { 22 | page: number 23 | limit: number 24 | total: number 25 | values: T[] 26 | } 27 | 28 | export type IErrorClustersResponse = {} & IPaginatedAPIResponse; 29 | 30 | export type IErrorCluster = { 31 | test_failure_id: string 32 | failure: string 33 | count: number 34 | } 35 | 36 | export type IFailureAnalysisMetric = { 37 | id: string 38 | name: string 39 | count: number 40 | } 41 | -------------------------------------------------------------------------------- /src/beats/index.js: -------------------------------------------------------------------------------- 1 | const TestResult = require('test-results-parser/src/models/TestResult'); 2 | const { Beats } = require('./beats'); 3 | 4 | /** 5 | * @param {import('../index').PublishReport} config 6 | * @param {TestResult} result 7 | */ 8 | async function run(config, result) { 9 | const beats = new Beats(config, result); 10 | await beats.publish(); 11 | } 12 | 13 | module.exports = { run } -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('dotenv').config(); 3 | 4 | const sade = require('sade'); 5 | 6 | const prog = sade('testbeats'); 7 | const { PublishCommand } = require('./commands/publish.command'); 8 | const { GenerateConfigCommand } = require('./commands/generate-config.command'); 9 | const logger = require('./utils/logger'); 10 | const pkg = require('../package.json'); 11 | 12 | prog 13 | .version(pkg.version) 14 | .option('-l, --logLevel', 'Log Level', "INFO") 15 | 16 | 17 | // Command to publish test results 18 | prog.command('publish') 19 | .option('-c, --config', 'path to config file') 20 | .option('--api-key', 'api key') 21 | .option('--project', 'project name') 22 | .option('--run', 'run name') 23 | .option('--slack', 'slack webhook url') 24 | .option('--teams', 'teams webhook url') 25 | .option('--chat', 'chat webhook url') 26 | .option('--title', 'title of the test run') 27 | .option('--junit', 'junit xml path') 28 | .option('--testng', 'testng xml path') 29 | .option('--cucumber', 'cucumber json path') 30 | .option('--mocha', 'mocha json path') 31 | .option('--nunit', 'nunit xml path') 32 | .option('--xunit', 'xunit xml path') 33 | .option('--mstest', 'mstest xml path') 34 | .option('-ci-info', 'ci info extension') 35 | .option('-chart-test-summary', 'chart test summary extension') 36 | .action(async (opts) => { 37 | try { 38 | logger.setLevel(opts.logLevel); 39 | const publish_command = new PublishCommand(opts); 40 | await publish_command.publish(); 41 | } catch (error) { 42 | logger.error(`Report publish failed: ${error.message}`); 43 | process.exit(1); 44 | } 45 | }); 46 | 47 | // Command to initialize and generate TestBeats Configuration file 48 | prog.command('init') 49 | .describe('Generate a TestBeats configuration file') 50 | .example('init') 51 | .action(async (opts) => { 52 | try { 53 | const generate_command = new GenerateConfigCommand(opts); 54 | await generate_command.execute(); 55 | } catch (error) { 56 | if (error.name === 'ExitPromptError') { 57 | logger.info('😿 Configuration generation was canceled by the user.'); 58 | } else { 59 | throw new Error(`❌ Error in generating configuration file: ${error.message}`) 60 | } 61 | process.exit(1); 62 | } 63 | }); 64 | 65 | prog.parse(process.argv); 66 | -------------------------------------------------------------------------------- /src/extensions/ai-failure-summary.extension.js: -------------------------------------------------------------------------------- 1 | const { BaseExtension } = require('./base.extension'); 2 | const { STATUS, HOOK } = require("../helpers/constants"); 3 | 4 | 5 | class AIFailureSummaryExtension extends BaseExtension { 6 | 7 | constructor(target, extension, result, payload, root_payload) { 8 | super(target, extension, result, payload, root_payload); 9 | this.#setDefaultOptions(); 10 | this.#setDefaultInputs(); 11 | this.updateExtensionInputs(); 12 | } 13 | 14 | run() { 15 | this.#setText(); 16 | this.attach(); 17 | } 18 | 19 | #setDefaultOptions() { 20 | this.default_options.hook = HOOK.AFTER_SUMMARY, 21 | this.default_options.condition = STATUS.PASS_OR_FAIL; 22 | } 23 | 24 | #setDefaultInputs() { 25 | this.default_inputs.title = 'AI Failure Summary ✨'; 26 | this.default_inputs.title_link = ''; 27 | } 28 | 29 | #setText() { 30 | const data = this.extension.inputs.data; 31 | if (!data) { 32 | return; 33 | } 34 | 35 | /** 36 | * @type {import('../beats/beats.types').IBeatExecutionMetric} 37 | */ 38 | const execution_metrics = data.execution_metrics[0]; 39 | this.text = execution_metrics.failure_summary; 40 | } 41 | } 42 | 43 | module.exports = { AIFailureSummaryExtension } -------------------------------------------------------------------------------- /src/extensions/base.extension.js: -------------------------------------------------------------------------------- 1 | const TestResult = require('test-results-parser/src/models/TestResult'); 2 | const logger = require('../utils/logger'); 3 | const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 4 | 5 | class BaseExtension { 6 | 7 | /** 8 | * 9 | * @param {import('..').ITarget} target 10 | * @param {import('..').IExtension} extension 11 | * @param {TestResult} result 12 | * @param {any} payload 13 | * @param {any} root_payload 14 | */ 15 | constructor(target, extension, result, payload, root_payload) { 16 | this.target = target; 17 | this.extension = extension; 18 | 19 | /** @type {TestResult} */ 20 | this.result = result; 21 | this.payload = payload; 22 | this.root_payload = root_payload; 23 | 24 | this.text = ''; 25 | 26 | /** 27 | * @type {import('..').ExtensionInputs} 28 | */ 29 | this.default_inputs = {}; 30 | 31 | /** 32 | * @type {import('..').IExtensionDefaultOptions} 33 | */ 34 | this.default_options = {}; 35 | } 36 | 37 | updateExtensionInputs() { 38 | this.extension.inputs = Object.assign({}, this.default_inputs, this.extension.inputs); 39 | switch (this.target.name) { 40 | case 'teams': 41 | this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs); 42 | break; 43 | case 'slack': 44 | this.extension.inputs = Object.assign({}, { separator: false }, this.extension.inputs); 45 | break; 46 | case 'chat': 47 | this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs); 48 | break; 49 | default: 50 | break; 51 | } 52 | } 53 | 54 | attach() { 55 | if (!this.text) { 56 | logger.debug(`⚠️ Extension '${this.extension.name}' has no text. Skipping.`); 57 | return; 58 | } 59 | 60 | switch (this.target.name) { 61 | case 'teams': 62 | addTeamsExtension({ payload: this.payload, extension: this.extension, text: this.text }); 63 | break; 64 | case 'slack': 65 | addSlackExtension({ payload: this.payload, extension: this.extension, text: this.text }); 66 | break; 67 | case 'chat': 68 | addChatExtension({ payload: this.payload, extension: this.extension, text: this.text }); 69 | break; 70 | default: 71 | break; 72 | } 73 | } 74 | 75 | /** 76 | * @param {string[]} texts 77 | */ 78 | mergeTexts(texts) { 79 | const _texts = texts.filter(text => !!text); 80 | switch (this.target.name) { 81 | case 'teams': 82 | return _texts.join('\n\n'); 83 | case 'slack': 84 | return _texts.join('\n'); 85 | case 'chat': 86 | return _texts.join('
'); 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | /** 93 | * @param {string|number} text 94 | */ 95 | bold(text) { 96 | switch (this.target.name) { 97 | case 'teams': 98 | return `**${text}**`; 99 | case 'slack': 100 | return `*${text}*`; 101 | case 'chat': 102 | return `${text}`; 103 | default: 104 | break; 105 | } 106 | } 107 | 108 | } 109 | 110 | module.exports = { BaseExtension } -------------------------------------------------------------------------------- /src/extensions/browserstack.extension.js: -------------------------------------------------------------------------------- 1 | const { HOOK, STATUS } = require("../helpers/constants"); 2 | const { getMetaDataText } = require("../helpers/metadata.helper"); 3 | const { BaseExtension } = require("./base.extension"); 4 | 5 | 6 | class BrowserstackExtension extends BaseExtension { 7 | constructor(target, extension, result, payload, root_payload) { 8 | super(target, extension, result, payload, root_payload); 9 | this.#setDefaultOptions(); 10 | } 11 | 12 | #setDefaultOptions() { 13 | this.default_options.hook = HOOK.AFTER_SUMMARY; 14 | this.default_options.condition = STATUS.PASS_OR_FAIL; 15 | } 16 | 17 | async run() { 18 | this.extension.inputs = Object.assign({}, this.extension.inputs); 19 | /** @type {import('../index').BrowserstackInputs} */ 20 | const inputs = this.extension.inputs; 21 | if (inputs.automation_build) { 22 | const element = { label: 'Browserstack', key: inputs.automation_build.name, value: inputs.automation_build.public_url, type: 'hyperlink' } 23 | const text = await getMetaDataText({ elements: [element], target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition }); 24 | this.text = text; 25 | this.attach(); 26 | } 27 | } 28 | } 29 | 30 | module.exports = { BrowserstackExtension }; -------------------------------------------------------------------------------- /src/extensions/ci-info.extension.js: -------------------------------------------------------------------------------- 1 | const { BaseExtension } = require("./base.extension"); 2 | const { getCIInformation } = require('../helpers/ci'); 3 | const { getMetaDataText } = require("../helpers/metadata.helper"); 4 | const { STATUS, HOOK } = require("../helpers/constants"); 5 | 6 | const COMMON_BRANCH_NAMES = ['main', 'master', 'dev', 'develop', 'qa', 'test']; 7 | 8 | class CIInfoExtension extends BaseExtension { 9 | 10 | constructor(target, extension, result, payload, root_payload) { 11 | super(target, extension, result, payload, root_payload); 12 | this.#setDefaultOptions(); 13 | this.#setDefaultInputs(); 14 | this.updateExtensionInputs(); 15 | 16 | this.ci = null; 17 | this.repository_elements = []; 18 | this.build_elements = []; 19 | } 20 | 21 | #setDefaultOptions() { 22 | this.default_options.hook = HOOK.AFTER_SUMMARY; 23 | this.default_options.condition = STATUS.PASS_OR_FAIL; 24 | } 25 | 26 | #setDefaultInputs() { 27 | this.default_inputs.title = ''; 28 | this.default_inputs.title_link = ''; 29 | this.default_inputs.show_repository_non_common = true; 30 | this.default_inputs.show_repository = false; 31 | this.default_inputs.show_repository_branch = false; 32 | this.default_inputs.show_build = true; 33 | } 34 | 35 | async run() { 36 | this.ci = getCIInformation(); 37 | 38 | this.#setRepositoryElements(); 39 | this.#setBuildElements(); 40 | 41 | const repository_text = await getMetaDataText({ elements: this.repository_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition }); 42 | const build_text = await getMetaDataText({ elements: this.build_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition }); 43 | this.text = this.mergeTexts([repository_text, build_text]); 44 | this.attach(); 45 | } 46 | 47 | #setRepositoryElements() { 48 | if (!this.ci) { 49 | return; 50 | } 51 | if (!this.ci.repository_url || !this.ci.repository_name || !this.ci.repository_ref) { 52 | return; 53 | } 54 | 55 | if (this.extension.inputs.show_repository) { 56 | this.#setRepositoryElement(); 57 | } 58 | if (this.extension.inputs.show_repository_branch) { 59 | if (this.ci.pull_request_name) { 60 | this.#setPullRequestElement(); 61 | } else { 62 | this.#setRepositoryBranchElement(); 63 | } 64 | } 65 | if (!this.extension.inputs.show_repository && !this.extension.inputs.show_repository_branch && this.extension.inputs.show_repository_non_common) { 66 | if (this.ci.pull_request_name) { 67 | this.#setRepositoryElement(); 68 | this.#setPullRequestElement(); 69 | } else { 70 | if (!COMMON_BRANCH_NAMES.includes(this.ci.branch_name.toLowerCase())) { 71 | this.#setRepositoryElement(); 72 | this.#setRepositoryBranchElement(); 73 | } 74 | } 75 | } 76 | } 77 | 78 | #setRepositoryElement() { 79 | this.repository_elements.push({ label: 'Repository', key: this.ci.repository_name, value: this.ci.repository_url, type: 'hyperlink' }); 80 | } 81 | 82 | #setPullRequestElement() { 83 | this.repository_elements.push({ label: 'Pull Request', key: this.ci.pull_request_name, value: this.ci.pull_request_url, type: 'hyperlink' }); 84 | } 85 | 86 | #setRepositoryBranchElement() { 87 | this.repository_elements.push({ label: 'Branch', key: this.ci.branch_name, value: this.ci.branch_url, type: 'hyperlink' }); 88 | } 89 | 90 | #setBuildElements() { 91 | if (!this.ci) { 92 | return; 93 | } 94 | 95 | if (this.extension.inputs.show_build && this.ci.build_url) { 96 | const name = (this.ci.build_name || 'Build') + (this.ci.build_number ? ` #${this.ci.build_number}` : ''); 97 | this.build_elements.push({ label: 'Build', key: name, value: this.ci.build_url, type: 'hyperlink' }); 98 | } 99 | if (this.extension.inputs.data) { 100 | this.build_elements = this.build_elements.concat(this.extension.inputs.data); 101 | } 102 | } 103 | 104 | } 105 | 106 | module.exports = { CIInfoExtension }; -------------------------------------------------------------------------------- /src/extensions/custom.extension.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { BaseExtension } = require("./base.extension"); 3 | const { STATUS, HOOK } = require("../helpers/constants"); 4 | 5 | class CustomExtension extends BaseExtension { 6 | 7 | /** 8 | * @param {import('..').CustomExtension} extension 9 | */ 10 | constructor(target, extension, result, payload, root_payload) { 11 | super(target, extension, result, payload, root_payload); 12 | this.extension = extension; 13 | this.#setDefaultOptions(); 14 | this.updateExtensionInputs(); 15 | } 16 | 17 | #setDefaultOptions() { 18 | this.default_options.hook = HOOK.END, 19 | this.default_options.condition = STATUS.PASS_OR_FAIL; 20 | } 21 | 22 | async run() { 23 | const params = this.#getParams(); 24 | if (typeof this.extension.inputs.load === 'string') { 25 | const cwd = process.cwd(); 26 | const extension_runner = require(path.join(cwd, this.extension.inputs.load)); 27 | await extension_runner.run(params); 28 | } else if (typeof this.extension.inputs.load === 'function') { 29 | await this.extension.inputs.load(params); 30 | } else { 31 | throw `Invalid 'load' input in custom extension - ${this.extension.inputs.load}`; 32 | } 33 | } 34 | 35 | #getParams() { 36 | return { 37 | target: this.target, 38 | extension: this.extension, 39 | payload: this.payload, 40 | root_payload: this.root_payload, 41 | result: this.result 42 | } 43 | } 44 | 45 | } 46 | 47 | module.exports = { CustomExtension } -------------------------------------------------------------------------------- /src/extensions/error-clusters.extension.js: -------------------------------------------------------------------------------- 1 | const { BaseExtension } = require('./base.extension'); 2 | const { STATUS, HOOK } = require("../helpers/constants"); 3 | const { truncate } = require('../helpers/helper'); 4 | 5 | class ErrorClustersExtension extends BaseExtension { 6 | 7 | constructor(target, extension, result, payload, root_payload) { 8 | super(target, extension, result, payload, root_payload); 9 | this.#setDefaultOptions(); 10 | this.#setDefaultInputs(); 11 | this.updateExtensionInputs(); 12 | } 13 | 14 | run() { 15 | this.#setText(); 16 | this.attach(); 17 | } 18 | 19 | #setDefaultOptions() { 20 | this.default_options.hook = HOOK.AFTER_SUMMARY, 21 | this.default_options.condition = STATUS.PASS_OR_FAIL; 22 | } 23 | 24 | #setDefaultInputs() { 25 | this.default_inputs.title = 'Top Errors'; 26 | this.default_inputs.title_link = ''; 27 | } 28 | 29 | #setText() { 30 | const data = this.extension.inputs.data; 31 | if (!data || !data.length) { 32 | return; 33 | } 34 | 35 | const clusters = data; 36 | 37 | const texts = []; 38 | for (const cluster of clusters) { 39 | texts.push(`${truncate(cluster.failure, 150)} - ${this.bold(`(x${cluster.count})`)}`); 40 | } 41 | this.text = this.mergeTexts(texts); 42 | } 43 | } 44 | 45 | module.exports = { ErrorClustersExtension } -------------------------------------------------------------------------------- /src/extensions/extensions.d.ts: -------------------------------------------------------------------------------- 1 | export type ICIInfo = { 2 | ci: string 3 | git: string 4 | repository_url: string 5 | repository_name: string 6 | repository_ref: string 7 | repository_commit_sha: string 8 | branch_url: string 9 | branch_name: string 10 | pull_request_url: string 11 | pull_request_name: string 12 | build_url: string 13 | build_number: string 14 | build_name: string 15 | user: string 16 | } -------------------------------------------------------------------------------- /src/extensions/failure-analysis.extension.js: -------------------------------------------------------------------------------- 1 | const { BaseExtension } = require('./base.extension'); 2 | const { STATUS, HOOK } = require("../helpers/constants"); 3 | 4 | class FailureAnalysisExtension extends BaseExtension { 5 | 6 | constructor(target, extension, result, payload, root_payload) { 7 | super(target, extension, result, payload, root_payload); 8 | this.#setDefaultOptions(); 9 | this.#setDefaultInputs(); 10 | this.updateExtensionInputs(); 11 | } 12 | 13 | #setDefaultOptions() { 14 | this.default_options.hook = HOOK.AFTER_SUMMARY, 15 | this.default_options.condition = STATUS.PASS_OR_FAIL; 16 | } 17 | 18 | #setDefaultInputs() { 19 | this.default_inputs.title = ''; 20 | this.default_inputs.title_link = ''; 21 | } 22 | 23 | run() { 24 | this.#setText(); 25 | this.attach(); 26 | } 27 | 28 | #setText() { 29 | /** 30 | * @type {import('../beats/beats.types').IFailureAnalysisMetric[]} 31 | */ 32 | const metrics = this.extension.inputs.data; 33 | if (!metrics || metrics.length === 0) { 34 | logger.warn('⚠️ No failure analysis metrics found. Skipping.'); 35 | return; 36 | } 37 | 38 | const to_investigate = metrics.find(metric => metric.name === 'To Investigate'); 39 | const auto_analysed = metrics.find(metric => metric.name === 'Auto Analysed'); 40 | 41 | const failure_analysis = []; 42 | 43 | if (to_investigate && to_investigate.count > 0) { 44 | failure_analysis.push(`🔎 To Investigate: ${to_investigate.count}`); 45 | } 46 | if (auto_analysed && auto_analysed.count > 0) { 47 | failure_analysis.push(`🪄 Auto Analysed: ${auto_analysed.count}`); 48 | } 49 | 50 | if (failure_analysis.length === 0) { 51 | return; 52 | } 53 | 54 | this.text = failure_analysis.join('    '); 55 | } 56 | 57 | } 58 | 59 | module.exports = { FailureAnalysisExtension }; -------------------------------------------------------------------------------- /src/extensions/hyperlinks.js: -------------------------------------------------------------------------------- 1 | const { STATUS, HOOK } = require("../helpers/constants"); 2 | const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 3 | const { getTeamsMetaDataText, getSlackMetaDataText, getChatMetaDataText } = require("../helpers/metadata.helper"); 4 | 5 | async function run({ target, extension, payload, result }) { 6 | const elements = get_elements(extension.inputs.links); 7 | if (target.name === 'teams') { 8 | extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); 9 | const text = await getTeamsMetaDataText({ elements, target, extension, result, default_condition: default_options.condition }); 10 | if (text) { 11 | addTeamsExtension({ payload, extension, text }); 12 | } 13 | } else if (target.name === 'slack') { 14 | extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); 15 | extension.inputs.block_type = 'context'; 16 | const text = await getSlackMetaDataText({ elements, target, extension, result, default_condition: default_options.condition }); 17 | if (text) { 18 | addSlackExtension({ payload, extension, text }); 19 | } 20 | } else if (target.name === 'chat') { 21 | extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); 22 | const text = await getChatMetaDataText({ elements, target, extension, result, default_condition: default_options.condition }); 23 | if (text) { 24 | addChatExtension({ payload, extension, text }); 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * 31 | * @param {import("..").Link[]} links 32 | */ 33 | function get_elements(links) { 34 | return links.map(_ => { return { key: _.text, value: _.url, type: 'hyperlink', condition: _.condition } }); 35 | } 36 | 37 | const default_options = { 38 | hook: HOOK.END, 39 | condition: STATUS.PASS_OR_FAIL, 40 | } 41 | 42 | const default_inputs_teams = { 43 | title: '', 44 | separator: true 45 | } 46 | 47 | const default_inputs_slack = { 48 | title: '', 49 | separator: false 50 | } 51 | 52 | const default_inputs_chat = { 53 | title: '', 54 | separator: true 55 | } 56 | 57 | module.exports = { 58 | run, 59 | default_options 60 | } -------------------------------------------------------------------------------- /src/extensions/index.js: -------------------------------------------------------------------------------- 1 | const hyperlinks = require('./hyperlinks'); 2 | const mentions = require('./mentions'); 3 | const rp_analysis = require('./report-portal-analysis'); 4 | const rp_history = require('./report-portal-history'); 5 | const qc_test_summary = require('./quick-chart-test-summary'); 6 | const percy_analysis = require('./percy-analysis'); 7 | const metadata = require('./metadata'); 8 | const { AIFailureSummaryExtension } = require('./ai-failure-summary.extension'); 9 | const { SmartAnalysisExtension } = require('./smart-analysis.extension'); 10 | const { CIInfoExtension } = require('./ci-info.extension'); 11 | const { CustomExtension } = require('./custom.extension'); 12 | const { EXTENSION } = require('../helpers/constants'); 13 | const { checkCondition } = require('../helpers/helper'); 14 | const logger = require('../utils/logger'); 15 | const { ErrorClustersExtension } = require('./error-clusters.extension'); 16 | const { FailureAnalysisExtension } = require('./failure-analysis.extension'); 17 | const { BrowserstackExtension } = require('./browserstack.extension'); 18 | 19 | async function run(options) { 20 | const { target, result, hook } = options; 21 | /** 22 | * @type {import("..").IExtension[]} 23 | */ 24 | const extensions = target.extensions || []; 25 | for (let i = 0; i < extensions.length; i++) { 26 | const extension = extensions[i]; 27 | if (extension.enable === false || extension.enable === 'false') { 28 | continue; 29 | } 30 | const extension_runner = getExtensionRunner(extension, options); 31 | const extension_options = Object.assign({}, extension_runner.default_options, extension); 32 | if (extension_options.hook === hook) { 33 | if (await checkCondition({ condition: extension_options.condition, result, target, extension })) { 34 | extension.outputs = {}; 35 | options.extension = extension; 36 | try { 37 | await extension_runner.run(options); 38 | } catch (error) { 39 | logger.error(`Failed to run extension: ${error.message}`); 40 | logger.debug(`Extension details`, extension); 41 | logger.debug(`Error: `, error); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | function getExtensionRunner(extension, options) { 49 | switch (extension.name) { 50 | case EXTENSION.HYPERLINKS: 51 | return hyperlinks; 52 | case EXTENSION.MENTIONS: 53 | return mentions; 54 | case EXTENSION.REPORT_PORTAL_ANALYSIS: 55 | return rp_analysis; 56 | case EXTENSION.REPORT_PORTAL_HISTORY: 57 | return rp_history; 58 | case EXTENSION.QUICK_CHART_TEST_SUMMARY: 59 | return qc_test_summary; 60 | case EXTENSION.PERCY_ANALYSIS: 61 | return percy_analysis; 62 | case EXTENSION.CUSTOM: 63 | return new CustomExtension(options.target, extension, options.result, options.payload, options.root_payload); 64 | case EXTENSION.METADATA: 65 | return metadata; 66 | case EXTENSION.CI_INFO: 67 | return new CIInfoExtension(options.target, extension, options.result, options.payload, options.root_payload); 68 | case EXTENSION.AI_FAILURE_SUMMARY: 69 | return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload); 70 | case EXTENSION.FAILURE_ANALYSIS: 71 | return new FailureAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload); 72 | case EXTENSION.SMART_ANALYSIS: 73 | return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload); 74 | case EXTENSION.ERROR_CLUSTERS: 75 | return new ErrorClustersExtension(options.target, extension, options.result, options.payload, options.root_payload); 76 | case EXTENSION.BROWSERSTACK: 77 | return new BrowserstackExtension(options.target, extension, options.result, options.payload, options.root_payload); 78 | default: 79 | return require(extension.name); 80 | } 81 | } 82 | 83 | module.exports = { 84 | run 85 | } -------------------------------------------------------------------------------- /src/extensions/mentions.js: -------------------------------------------------------------------------------- 1 | const { getOnCallPerson } = require('rosters'); 2 | const { addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 3 | const { HOOK, STATUS } = require('../helpers/constants'); 4 | 5 | function run({ target, extension, payload, root_payload }) { 6 | if (target.name === 'teams') { 7 | extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); 8 | attachForTeam({ extension, payload }); 9 | } else if (target.name === 'slack') { 10 | extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); 11 | attachForSlack({ extension, payload }); 12 | } else if (target.name === 'chat') { 13 | extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); 14 | attachForChat({ extension, root_payload }); 15 | } 16 | } 17 | 18 | function attachForTeam({ extension, payload }) { 19 | const users = getUsers(extension); 20 | if (users.length > 0) { 21 | setPayloadWithMSTeamsEntities(payload); 22 | const users_ats = users.map(user => `${user.name}`); 23 | addTeamsExtension({ payload, extension, text: users_ats.join(' | ')}); 24 | for (const user of users) { 25 | payload.msteams.entities.push({ 26 | "type": "mention", 27 | "text": `${user.name}`, 28 | "mentioned": { 29 | "id": user.teams_upn, 30 | "name": user.name 31 | } 32 | }); 33 | } 34 | } 35 | } 36 | 37 | function formatSlackMentions({slack_uid, slack_gid}) { 38 | if (slack_gid && slack_uid) { 39 | throw new Error(`Error in slack extension configuration. Either slack user or group Id is allowed`); 40 | } 41 | if (slack_uid) { 42 | return `<@${slack_uid}>` 43 | } 44 | const tagPrefix = ["here", "everyone", "channel"].includes(slack_gid.toLowerCase()) ? "" : "subteam^"; 45 | return `` 46 | } 47 | 48 | function attachForSlack({ extension, payload }) { 49 | const users = getUsers(extension); 50 | const user_ids = users.map(formatSlackMentions); 51 | if (users.length > 0) { 52 | addSlackExtension({ payload, extension, text: user_ids.join(' | ') }); 53 | } 54 | } 55 | 56 | function attachForChat({ extension, root_payload }) { 57 | const users = getUsers(extension); 58 | const user_ids = users.map(user => ``); 59 | if (users.length > 0) { 60 | root_payload.text = user_ids.join(' | '); 61 | } 62 | } 63 | 64 | function getUsers(extension) { 65 | const users = []; 66 | if (extension.inputs.users) { 67 | users.push(...extension.inputs.users); 68 | } 69 | if (extension.inputs.schedule) { 70 | const user = getOnCallPerson(extension.inputs.schedule); 71 | if (user) { 72 | users.push(user); 73 | } else { 74 | // TODO: warn message for no on-call person 75 | } 76 | } 77 | return users; 78 | } 79 | 80 | function setPayloadWithMSTeamsEntities(payload) { 81 | if (!payload.msteams) { 82 | payload.msteams = {}; 83 | } 84 | if (!payload.msteams.entities) { 85 | payload.msteams.entities = []; 86 | } 87 | } 88 | 89 | const default_options = { 90 | hook: HOOK.AFTER_SUMMARY, 91 | condition: STATUS.FAIL 92 | } 93 | 94 | const default_inputs_teams = { 95 | title: '', 96 | separator: true 97 | } 98 | 99 | const default_inputs_slack = { 100 | title: '', 101 | separator: false 102 | } 103 | 104 | const default_inputs_chat = { 105 | title: '', 106 | separator: true 107 | } 108 | 109 | module.exports = { 110 | run, 111 | default_options 112 | } -------------------------------------------------------------------------------- /src/extensions/metadata.js: -------------------------------------------------------------------------------- 1 | const { HOOK, STATUS } = require('../helpers/constants'); 2 | const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 3 | const { getTeamsMetaDataText, getSlackMetaDataText, getChatMetaDataText } = require('../helpers/metadata.helper'); 4 | 5 | /** 6 | * @param {object} param0 7 | * @param {import('..').ITarget} param0.target 8 | * @param {import('..').MetadataExtension} param0.extension 9 | */ 10 | async function run({ target, extension, result, payload, root_payload }) { 11 | if (target.name === 'teams') { 12 | extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); 13 | await attachForTeams({ target, extension, payload, result }); 14 | } else if (target.name === 'slack') { 15 | extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); 16 | await attachForSlack({ target, extension, payload, result }); 17 | } else if (target.name === 'chat') { 18 | extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); 19 | await attachForChat({ target, extension, payload, result }); 20 | } 21 | } 22 | 23 | /** 24 | * @param {object} param0 25 | * @param {import('..').MetadataExtension} param0.extension 26 | */ 27 | async function attachForTeams({ target, extension, payload, result }) { 28 | const text = await getTeamsMetaDataText({ 29 | elements: extension.inputs.data, 30 | target, 31 | extension, 32 | result, 33 | default_condition: default_options.condition 34 | }); 35 | if (text) { 36 | addTeamsExtension({ payload, extension, text }); 37 | } 38 | } 39 | 40 | async function attachForSlack({ target, extension, payload, result }) { 41 | const text = await getSlackMetaDataText({ 42 | elements: extension.inputs.data, 43 | target, 44 | extension, 45 | result, 46 | default_condition: default_options.condition 47 | }); 48 | if (text) { 49 | addSlackExtension({ payload, extension, text }); 50 | } 51 | } 52 | 53 | async function attachForChat({ target, extension, payload, result }) { 54 | const text = await getChatMetaDataText({ 55 | elements: extension.inputs.data, 56 | target, 57 | extension, 58 | result, 59 | default_condition: default_options.condition 60 | }); 61 | if (text) { 62 | addChatExtension({ payload, extension, text }); 63 | } 64 | } 65 | 66 | const default_options = { 67 | hook: HOOK.END, 68 | condition: STATUS.PASS_OR_FAIL 69 | } 70 | 71 | const default_inputs_teams = { 72 | title: '', 73 | separator: true 74 | } 75 | 76 | const default_inputs_slack = { 77 | title: '', 78 | separator: false 79 | } 80 | 81 | const default_inputs_chat = { 82 | title: '', 83 | separator: true 84 | } 85 | 86 | module.exports = { 87 | run, 88 | default_options 89 | } -------------------------------------------------------------------------------- /src/extensions/quick-chart-test-summary.js: -------------------------------------------------------------------------------- 1 | const { getPercentage } = require('../helpers/helper'); 2 | const { HOOK, STATUS, URLS } = require('../helpers/constants'); 3 | 4 | function getUrl(extension, result) { 5 | const percentage = getPercentage(result.passed, result.total); 6 | const chart = { 7 | type: 'radialGauge', 8 | data: { 9 | datasets: [{ 10 | data: [percentage], 11 | backgroundColor: 'green', 12 | }] 13 | }, 14 | options: { 15 | trackColor: '#FF0000', 16 | roundedCorners: false, 17 | centerPercentage: 80, 18 | centerArea: { 19 | fontSize: 74, 20 | text: `${percentage}%`, 21 | }, 22 | } 23 | } 24 | return `${extension.inputs.url}/chart?c=${encodeURIComponent(JSON.stringify(chart))}`; 25 | } 26 | 27 | function attachForTeams({ extension, result, payload }) { 28 | const main_column = { 29 | "type": "Column", 30 | "items": [payload.body[0], payload.body[1]], 31 | "width": "stretch" 32 | } 33 | const image_column = { 34 | "type": "Column", 35 | "items": [ 36 | { 37 | "type": "Image", 38 | "url": getUrl(extension, result), 39 | "altText": "overall-results-summary", 40 | "size": "large" 41 | } 42 | ], 43 | "width": "auto" 44 | } 45 | const column_set = { 46 | "type": "ColumnSet", 47 | "columns": [ 48 | main_column, 49 | image_column 50 | ] 51 | } 52 | payload.body = [column_set]; 53 | } 54 | 55 | function attachForSlack({ extension, result, payload }) { 56 | payload.blocks[0]["accessory"] = { 57 | "type": "image", 58 | "alt_text": "overall-results-summary", 59 | "image_url": getUrl(extension, result) 60 | } 61 | } 62 | 63 | function run(params) { 64 | const { extension, target } = params; 65 | params.extension.inputs = extension.inputs || {}; 66 | params.extension.inputs["url"] = (extension.inputs.url && extension.inputs.url.trim()) || URLS.QUICK_CHART; 67 | if (target.name === 'teams') { 68 | attachForTeams(params); 69 | } else if (target.name === 'slack') { 70 | attachForSlack(params); 71 | } 72 | } 73 | 74 | const default_options = { 75 | hook: HOOK.AFTER_SUMMARY, 76 | condition: STATUS.PASS_OR_FAIL 77 | } 78 | 79 | module.exports = { 80 | run, 81 | default_options 82 | } -------------------------------------------------------------------------------- /src/extensions/report-portal-analysis.js: -------------------------------------------------------------------------------- 1 | const { getLaunchDetails, getLastLaunchByName } = require('../helpers/report-portal'); 2 | const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 3 | const { HOOK, STATUS } = require('../helpers/constants'); 4 | 5 | function getReportPortalDefectsSummary(defects, bold_start = '**', bold_end = '**') { 6 | const results = []; 7 | if (defects.product_bug) { 8 | results.push(`${bold_start}🔴 PB - ${defects.product_bug.total}${bold_end}`); 9 | } else { 10 | results.push(`🔴 PB - 0`); 11 | } 12 | if (defects.automation_bug) { 13 | results.push(`${bold_start}🟡 AB - ${defects.automation_bug.total}${bold_end}`); 14 | } else { 15 | results.push(`🟡 AB - 0`); 16 | } 17 | if (defects.system_issue) { 18 | results.push(`${bold_start}🔵 SI - ${defects.system_issue.total}${bold_end}`); 19 | } else { 20 | results.push(`🔵 SI - 0`); 21 | } 22 | if (defects.no_defect) { 23 | results.push(`${bold_start}◯ ND - ${defects.no_defect.total}${bold_end}`); 24 | } else { 25 | results.push(`◯ ND - 0`); 26 | } 27 | if (defects.to_investigate) { 28 | results.push(`${bold_start}🟠 TI - ${defects.to_investigate.total}${bold_end}`); 29 | } else { 30 | results.push(`🟠 TI - 0`); 31 | } 32 | return results; 33 | } 34 | 35 | async function _getLaunchDetails(options) { 36 | if (!options.launch_id && options.launch_name) { 37 | return getLastLaunchByName(options); 38 | } 39 | return getLaunchDetails(options); 40 | } 41 | 42 | async function initialize(extension) { 43 | extension.inputs = Object.assign({}, default_inputs, extension.inputs); 44 | extension.outputs.launch = await _getLaunchDetails(extension.inputs); 45 | if (!extension.inputs.title_link && extension.inputs.title_link_to_launch) { 46 | extension.inputs.title_link = `${extension.inputs.url}/ui/#${extension.inputs.project}/launches/all/${extension.outputs.launch.uuid}`; 47 | } 48 | } 49 | 50 | async function run({ extension, payload, target }) { 51 | await initialize(extension); 52 | const { statistics } = extension.outputs.launch; 53 | if (statistics && statistics.defects) { 54 | if (target.name === 'teams') { 55 | extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); 56 | const analyses = getReportPortalDefectsSummary(statistics.defects); 57 | addTeamsExtension({ payload, extension, text: analyses.join(' | ') }); 58 | } else if (target.name === 'slack') { 59 | extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); 60 | const analyses = getReportPortalDefectsSummary(statistics.defects, '*', '*'); 61 | addSlackExtension({ payload, extension, text: analyses.join(' | ') }); 62 | } else if (target.name === 'chat') { 63 | extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); 64 | const analyses = getReportPortalDefectsSummary(statistics.defects, '', ''); 65 | addChatExtension({ payload, extension, text: analyses.join(' | ') }); 66 | } 67 | } 68 | } 69 | 70 | const default_options = { 71 | hook: HOOK.END, 72 | condition: STATUS.FAIL 73 | } 74 | 75 | const default_inputs = { 76 | title: 'Report Portal Analysis', 77 | title_link_to_launch: true, 78 | } 79 | 80 | const default_inputs_teams = { 81 | separator: true 82 | } 83 | 84 | const default_inputs_slack = { 85 | separator: false 86 | } 87 | 88 | const default_inputs_chat = { 89 | separator: true 90 | } 91 | 92 | module.exports = { 93 | run, 94 | default_options 95 | } -------------------------------------------------------------------------------- /src/extensions/report-portal-history.js: -------------------------------------------------------------------------------- 1 | const { getSuiteHistory, getLastLaunchByName, getLaunchDetails } = require('../helpers/report-portal'); 2 | const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); 3 | const { HOOK, STATUS } = require('../helpers/constants'); 4 | const logger = require('../utils/logger'); 5 | 6 | async function getLaunchHistory(extension) { 7 | const { inputs, outputs } = extension; 8 | if (!inputs.launch_id && inputs.launch_name) { 9 | const launch = await getLastLaunchByName(inputs); 10 | outputs.launch = launch; 11 | inputs.launch_id = launch.id; 12 | } 13 | if (typeof inputs.launch_id === 'string') { 14 | const launch = await getLaunchDetails(inputs); 15 | outputs.launch = launch; 16 | inputs.launch_id = launch.id; 17 | } 18 | const response = await getSuiteHistory(inputs); 19 | if (response.content.length > 0) { 20 | outputs.history = response.content[0].resources; 21 | return response.content[0].resources; 22 | } 23 | return []; 24 | } 25 | 26 | function getSymbols({ target, extension, launches }) { 27 | const symbols = []; 28 | for (let i = 0; i < launches.length; i++) { 29 | const launch = launches[i]; 30 | const launch_url = `${extension.inputs.url}/ui/#${extension.inputs.project}/launches/all/${launch[extension.inputs.link_history_via]}`; 31 | let current_symbol = '⚠️'; 32 | if (launch.status === 'PASSED') { 33 | current_symbol = '✅'; 34 | } else if (launch.status === 'FAILED') { 35 | current_symbol = '❌'; 36 | } 37 | if (target.name === 'teams') { 38 | symbols.push(`[${current_symbol}](${launch_url})`); 39 | } else if (target.name === 'slack') { 40 | symbols.push(`<${launch_url}|${current_symbol}>`); 41 | } else if (target.name === 'chat') { 42 | symbols.push(`${current_symbol}`); 43 | } else { 44 | symbols.push(current_symbol); 45 | } 46 | } 47 | return symbols; 48 | } 49 | 50 | function setTitle(extension, symbols) { 51 | if (extension.inputs.title === 'Last Runs') { 52 | extension.inputs.title = `Last ${symbols.length} Runs` 53 | } 54 | } 55 | 56 | async function run({ extension, target, payload }) { 57 | try { 58 | extension.inputs = Object.assign({}, default_inputs, extension.inputs); 59 | const launches = await getLaunchHistory(extension); 60 | const symbols = getSymbols({ target, extension, launches }); 61 | if (symbols.length > 0) { 62 | setTitle(extension, symbols); 63 | if (target.name === 'teams') { 64 | extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); 65 | addTeamsExtension({ payload, extension, text: symbols.join(' ') }); 66 | } else if (target.name === 'slack') { 67 | extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); 68 | addSlackExtension({ payload, extension, text: symbols.join(' ') }); 69 | } else if (target.name === 'chat') { 70 | extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); 71 | addChatExtension({ payload, extension, text: symbols.join(' ') }); 72 | } 73 | } 74 | } catch (error) { 75 | logger.error(`Failed to get report portal history: ${error.message}`); 76 | logger.debug(`Error: ${error}`); 77 | } 78 | } 79 | 80 | const default_inputs = { 81 | history_depth: 5, 82 | title: 'Last Runs', 83 | link_history_via: 'uuid' 84 | } 85 | 86 | const default_inputs_teams = { 87 | separator: true 88 | } 89 | 90 | const default_inputs_chat = { 91 | separator: true 92 | } 93 | 94 | const default_inputs_slack = { 95 | separator: false 96 | } 97 | 98 | const default_options = { 99 | hook: HOOK.END, 100 | condition: STATUS.FAIL 101 | } 102 | 103 | module.exports = { 104 | run, 105 | default_options 106 | } 107 | -------------------------------------------------------------------------------- /src/extensions/smart-analysis.extension.js: -------------------------------------------------------------------------------- 1 | const { BaseExtension } = require('./base.extension'); 2 | const { STATUS, HOOK } = require("../helpers/constants"); 3 | const logger = require('../utils/logger'); 4 | 5 | class SmartAnalysisExtension extends BaseExtension { 6 | 7 | constructor(target, extension, result, payload, root_payload) { 8 | super(target, extension, result, payload, root_payload); 9 | this.#setDefaultOptions(); 10 | this.#setDefaultInputs(); 11 | this.updateExtensionInputs(); 12 | } 13 | 14 | run() { 15 | this.#setText(); 16 | this.attach(); 17 | } 18 | 19 | #setDefaultOptions() { 20 | this.default_options.hook = HOOK.AFTER_SUMMARY, 21 | this.default_options.condition = STATUS.PASS_OR_FAIL; 22 | } 23 | 24 | #setDefaultInputs() { 25 | this.default_inputs.title = ''; 26 | this.default_inputs.title_link = ''; 27 | } 28 | 29 | #setText() { 30 | const data = this.extension.inputs.data; 31 | 32 | if (!data) { 33 | return; 34 | } 35 | 36 | /** 37 | * @type {import('../beats/beats.types').IBeatExecutionMetric} 38 | */ 39 | const execution_metrics = data.execution_metrics[0]; 40 | 41 | if (!execution_metrics) { 42 | logger.warn('⚠️ No execution metrics found. Skipping.'); 43 | return; 44 | } 45 | 46 | const smart_analysis = []; 47 | if (execution_metrics.newly_failed) { 48 | smart_analysis.push(`⭕ Newly Failed: ${execution_metrics.newly_failed}`); 49 | } 50 | if (execution_metrics.always_failing) { 51 | smart_analysis.push(`🔴 Always Failing: ${execution_metrics.always_failing}`); 52 | } 53 | if (execution_metrics.recurring_errors) { 54 | smart_analysis.push(`🟠 Recurring Errors: ${execution_metrics.recurring_errors}`); 55 | } 56 | if (execution_metrics.flaky) { 57 | smart_analysis.push(`🟡 Flaky: ${execution_metrics.flaky}`); 58 | } 59 | if (execution_metrics.recovered) { 60 | smart_analysis.push(`🟢 Recovered: ${execution_metrics.recovered}`); 61 | } 62 | 63 | const texts = []; 64 | const rows = []; 65 | for (const item of smart_analysis) { 66 | rows.push(item); 67 | if (rows.length === 3) { 68 | texts.push(rows.join('    ')); 69 | rows.length = 0; 70 | } 71 | } 72 | 73 | if (rows.length > 0) { 74 | texts.push(rows.join('    ')); 75 | } 76 | 77 | this.text = this.mergeTexts(texts); 78 | } 79 | 80 | } 81 | 82 | module.exports = { SmartAnalysisExtension }; -------------------------------------------------------------------------------- /src/helpers/browserstack.helper.js: -------------------------------------------------------------------------------- 1 | const request = require('phin-retry'); 2 | const { URLS } = require('./constants'); 3 | 4 | 5 | /** 6 | * 7 | * @param {import('../index').BrowserstackInputs} inputs 8 | */ 9 | function getBaseUrl(inputs) { 10 | return inputs.url || URLS.BROWSERSTACK; 11 | } 12 | 13 | /** 14 | * 15 | * @param {import('../index').BrowserstackInputs} inputs 16 | */ 17 | async function getAutomationBuilds(inputs) { 18 | return request.get({ 19 | url: `${getBaseUrl(inputs)}/automate/builds.json?limit=100`, 20 | auth: { 21 | username: inputs.username, 22 | password: inputs.access_key 23 | }, 24 | }); 25 | } 26 | 27 | /** 28 | * 29 | * @param {import('../index').BrowserstackInputs} inputs 30 | * @param {string} build_id 31 | */ 32 | async function getAutomationBuildSessions(inputs, build_id) { 33 | return request.get({ 34 | url: `${getBaseUrl(inputs)}/automate/builds/${build_id}/sessions.json`, 35 | auth: { 36 | username: inputs.username, 37 | password: inputs.access_key 38 | }, 39 | }); 40 | } 41 | 42 | module.exports = { 43 | getAutomationBuilds, 44 | getAutomationBuildSessions 45 | } 46 | -------------------------------------------------------------------------------- /src/helpers/ci/azure-devops.js: -------------------------------------------------------------------------------- 1 | const { BaseCI } = require('./base.ci'); 2 | 3 | const ENV = process.env; 4 | 5 | class AzureDevOpsCI extends BaseCI { 6 | constructor() { 7 | super(); 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.setInfo(this.targets.ci, 'AZURE_DEVOPS_PIPELINES'); 13 | this.setInfo(this.targets.git, 'AZURE_DEVOPS_REPOS'); 14 | this.setInfo(this.targets.repository_url, ENV.BUILD_REPOSITORY_URI); 15 | this.setInfo(this.targets.repository_name, ENV.BUILD_REPOSITORY_NAME); 16 | this.setInfo(this.targets.repository_ref, ENV.BUILD_SOURCEBRANCH); 17 | this.setInfo(this.targets.repository_commit_sha, ENV.BUILD_SOURCEVERSION); 18 | this.setInfo(this.targets.build_url, ENV.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI + ENV.SYSTEM_TEAMPROJECT + '/_build/results?buildId=' + ENV.BUILD_BUILDID); 19 | this.setInfo(this.targets.build_number, ENV.BUILD_BUILDNUMBER); 20 | this.setInfo(this.targets.build_name, ENV.BUILD_DEFINITIONNAME); 21 | this.setInfo(this.targets.build_reason, ENV.BUILD_REASON); 22 | 23 | this.setInfo(this.targets.branch_url, this.repository_url + this.repository_ref.replace('refs/heads/', '/tree/')); 24 | this.setInfo(this.targets.branch_name, this.repository_ref.replace('refs/heads/', '')); 25 | 26 | if (this.repository_ref.includes('refs/pull')) { 27 | this.setInfo(this.targets.pull_request_url, this.repository_url + this.repository_ref.replace('refs/pull/', '/pull/')); 28 | this.setInfo(this.targets.pull_request_name, this.repository_ref.replace('refs/pull/', '').replace('/merge', '')); 29 | } 30 | 31 | this.setInfo(this.targets.user, ENV.BUILD_REQUESTEDFOR, true); 32 | } 33 | } 34 | 35 | module.exports = { 36 | AzureDevOpsCI 37 | } 38 | -------------------------------------------------------------------------------- /src/helpers/ci/base.ci.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const pkg = require('../../../package.json'); 3 | 4 | const ENV = process.env; 5 | 6 | class BaseCI { 7 | ci = ''; 8 | git = ''; 9 | repository_url = ''; 10 | repository_name = ''; 11 | repository_ref = ''; 12 | repository_commit_sha = ''; 13 | branch_url = ''; 14 | branch_name = ''; 15 | pull_request_url = ''; 16 | pull_request_name = ''; 17 | build_url = ''; 18 | build_number = ''; 19 | build_name = ''; 20 | build_reason = ''; 21 | user = ''; 22 | runtime = ''; 23 | runtime_version = ''; 24 | os = ''; 25 | os_version = ''; 26 | testbeats_version = ''; 27 | 28 | targets = { 29 | ci: 'ci', 30 | git: 'git', 31 | repository_url: 'repository_url', 32 | repository_name: 'repository_name', 33 | repository_ref: 'repository_ref', 34 | repository_commit_sha: 'repository_commit_sha', 35 | branch_url: 'branch_url', 36 | branch_name: 'branch_name', 37 | pull_request_url: 'pull_request_url', 38 | pull_request_name: 'pull_request_name', 39 | build_url: 'build_url', 40 | build_number: 'build_number', 41 | build_name: 'build_name', 42 | build_reason: 'build_reason', 43 | user: 'user', 44 | runtime: 'runtime', 45 | runtime_version: 'runtime_version', 46 | os: 'os', 47 | os_version: 'os_version', 48 | testbeats_version: 'testbeats_version' 49 | } 50 | 51 | constructor() { 52 | this.setDefaultInformation(); 53 | } 54 | 55 | setDefaultInformation() { 56 | this.ci = ENV.TEST_BEATS_CI_NAME; 57 | this.git = ENV.TEST_BEATS_CI_GIT; 58 | this.repository_url = ENV.TEST_BEATS_CI_REPOSITORY_URL; 59 | this.repository_name = ENV.TEST_BEATS_CI_REPOSITORY_NAME; 60 | this.repository_ref = ENV.TEST_BEATS_CI_REPOSITORY_REF; 61 | this.repository_commit_sha = ENV.TEST_BEATS_CI_REPOSITORY_COMMIT_SHA; 62 | this.branch_url = ENV.TEST_BEATS_BRANCH_URL; 63 | this.branch_name = ENV.TEST_BEATS_BRANCH_NAME; 64 | this.pull_request_url = ENV.TEST_BEATS_PULL_REQUEST_URL; 65 | this.pull_request_name = ENV.TEST_BEATS_PULL_REQUEST_NAME; 66 | this.build_url = ENV.TEST_BEATS_CI_BUILD_URL; 67 | this.build_number = ENV.TEST_BEATS_CI_BUILD_NUMBER; 68 | this.build_name = ENV.TEST_BEATS_CI_BUILD_NAME; 69 | this.build_reason = ENV.TEST_BEATS_CI_BUILD_REASON; 70 | this.user = ENV.TEST_BEATS_CI_USER || os.userInfo().username; 71 | 72 | const runtime = this.#getRuntimeInfo(); 73 | this.runtime = runtime.name; 74 | this.runtime_version = runtime.version; 75 | this.os = os.platform(); 76 | this.os_version = os.release(); 77 | this.testbeats_version = pkg.version; 78 | 79 | } 80 | 81 | #getRuntimeInfo() { 82 | if (typeof process !== 'undefined' && process.versions && process.versions.node) { 83 | return { name: 'node', version: process.versions.node }; 84 | } else if (typeof Deno !== 'undefined') { 85 | return { name: 'deno', version: Deno.version.deno }; 86 | } else if (typeof Bun !== 'undefined') { 87 | return { name: 'bun', version: Bun.version }; 88 | } else { 89 | return { name: 'unknown', version: 'unknown' }; 90 | } 91 | } 92 | 93 | setInfo(target, value, force = false) { 94 | if (force && value) { 95 | this[target] = value; 96 | return; 97 | } 98 | if (!this[target]) { 99 | this[target] = value; 100 | } 101 | } 102 | 103 | /** 104 | * 105 | * @returns {import('../../extensions/extensions').ICIInfo} 106 | */ 107 | info() { 108 | return { 109 | ci: this.ci, 110 | git: this.git, 111 | repository_url: this.repository_url, 112 | repository_name: this.repository_name, 113 | repository_ref: this.repository_ref, 114 | repository_commit_sha: this.repository_commit_sha, 115 | branch_url: this.branch_url, 116 | branch_name: this.branch_name, 117 | pull_request_url: this.pull_request_url, 118 | pull_request_name: this.pull_request_name, 119 | build_url: this.build_url, 120 | build_number: this.build_number, 121 | build_name: this.build_name, 122 | build_reason: this.build_reason, 123 | user: this.user, 124 | runtime: this.runtime, 125 | runtime_version: this.runtime_version, 126 | os: this.os, 127 | os_version: this.os_version, 128 | testbeats_version: this.testbeats_version 129 | } 130 | } 131 | 132 | } 133 | 134 | module.exports = { BaseCI }; -------------------------------------------------------------------------------- /src/helpers/ci/circle-ci.js: -------------------------------------------------------------------------------- 1 | const { BaseCI } = require('./base.ci'); 2 | 3 | const ENV = process.env; 4 | 5 | class CircleCI extends BaseCI { 6 | constructor() { 7 | super(); 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.setInfo(this.targets.ci, 'CIRCLE_CI'); 13 | this.setInfo(this.targets.git, ''); 14 | this.setInfo(this.targets.repository_url, ENV.CIRCLE_REPOSITORY_URL); 15 | this.setInfo(this.targets.repository_name, ENV.CIRCLE_PROJECT_REPONAME); 16 | this.setInfo(this.targets.repository_ref, ENV.CIRCLE_BRANCH); 17 | this.setInfo(this.targets.repository_commit_sha, ENV.CIRCLE_SHA1); 18 | this.setInfo(this.targets.branch_url, ''); 19 | this.setInfo(this.targets.branch_name, ENV.CIRCLE_BRANCH); 20 | this.setInfo(this.targets.pull_request_url,''); 21 | this.setInfo(this.targets.pull_request_name, ''); 22 | this.setInfo(this.targets.build_url, ENV.CIRCLE_BUILD_URL); 23 | this.setInfo(this.targets.build_number, ENV.CIRCLE_BUILD_NUM); 24 | this.setInfo(this.targets.build_name, ENV.CIRCLE_JOB); 25 | this.setInfo(this.targets.build_reason, 'Push'); 26 | this.setInfo(this.targets.user, ENV.CIRCLE_USERNAME, true); 27 | } 28 | } 29 | 30 | module.exports = { 31 | CircleCI 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers/ci/github.js: -------------------------------------------------------------------------------- 1 | const { BaseCI } = require('./base.ci'); 2 | 3 | const ENV = process.env; 4 | 5 | class GitHubCI extends BaseCI { 6 | constructor() { 7 | super(); 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.setInfo(this.targets.ci, 'GITHUB_ACTIONS'); 13 | this.setInfo(this.targets.git, 'GITHUB'); 14 | this.setInfo(this.targets.repository_url, ENV.GITHUB_SERVER_URL + '/' + ENV.GITHUB_REPOSITORY); 15 | this.setInfo(this.targets.repository_name, ENV.GITHUB_REPOSITORY); 16 | this.setInfo(this.targets.repository_ref, ENV.GITHUB_REF); 17 | this.setInfo(this.targets.repository_commit_sha, ENV.GITHUB_SHA); 18 | this.setInfo(this.targets.build_url, ENV.GITHUB_SERVER_URL + '/' + ENV.GITHUB_REPOSITORY + '/actions/runs/' + ENV.GITHUB_RUN_ID); 19 | this.setInfo(this.targets.build_number, ENV.GITHUB_RUN_NUMBER); 20 | this.setInfo(this.targets.build_name, ENV.GITHUB_WORKFLOW); 21 | this.setInfo(this.targets.build_reason, ENV.GITHUB_EVENT_NAME); 22 | this.setInfo(this.targets.user, ENV.GITHUB_ACTOR, true); 23 | 24 | this.setInfo(this.targets.branch_url, this.repository_url + this.repository_ref.replace('refs/heads/', '/tree/')); 25 | this.setInfo(this.targets.branch_name, this.repository_ref.replace('refs/heads/', '')); 26 | 27 | if (this.repository_ref.includes('refs/pull')) { 28 | this.setInfo(this.targets.pull_request_url, this.repository_url + this.repository_ref.replace('refs/pull/', '/pull/')); 29 | this.setInfo(this.targets.pull_request_name, this.repository_ref.replace('refs/pull/', '').replace('/merge', '')); 30 | } 31 | } 32 | } 33 | 34 | module.exports = { 35 | GitHubCI 36 | } -------------------------------------------------------------------------------- /src/helpers/ci/gitlab.js: -------------------------------------------------------------------------------- 1 | const { BaseCI } = require('./base.ci'); 2 | 3 | const ENV = process.env; 4 | 5 | class GitLabCI extends BaseCI { 6 | constructor() { 7 | super(); 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.setInfo(this.targets.ci, 'GITLAB'); 13 | this.setInfo(this.targets.git, 'GITLAB'); 14 | this.setInfo(this.targets.repository_url, ENV.CI_PROJECT_URL); 15 | this.setInfo(this.targets.repository_name, ENV.CI_PROJECT_NAME); 16 | this.setInfo(this.targets.repository_ref, '/-/tree/' + (ENV.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME || ENV.CI_COMMIT_REF_NAME)); 17 | this.setInfo(this.targets.repository_commit_sha, ENV.CI_MERGE_REQUEST_SOURCE_BRANCH_SHA || ENV.CI_COMMIT_SHA); 18 | this.setInfo(this.targets.branch_url, ENV.CI_PROJECT_URL + '/-/tree/' + (ENV.CI_COMMIT_REF_NAME || ENV.CI_COMMIT_BRANCH)); 19 | this.setInfo(this.targets.branch_name, ENV.CI_COMMIT_REF_NAME || ENV.CI_COMMIT_BRANCH); 20 | this.setInfo(this.targets.pull_request_url,''); 21 | this.setInfo(this.targets.pull_request_name, ''); 22 | this.setInfo(this.targets.build_url, ENV.CI_JOB_URL); 23 | this.setInfo(this.targets.build_number, ENV.CI_JOB_ID); 24 | this.setInfo(this.targets.build_name, ENV.CI_JOB_NAME); 25 | this.setInfo(this.targets.build_reason, ENV.CI_PIPELINE_SOURCE); 26 | 27 | if (ENV.CI_OPEN_MERGE_REQUESTS) { 28 | const pr_number = ENV.CI_OPEN_MERGE_REQUESTS.split("!")[1]; 29 | this.setInfo(this.targets.pull_request_name, "#" + pr_number); 30 | this.setInfo(this.targets.pull_request_url, ENV.CI_PROJECT_URL + "/-/merge_requests/" + pr_number); 31 | } 32 | } 33 | } 34 | 35 | module.exports = { 36 | GitLabCI 37 | } 38 | -------------------------------------------------------------------------------- /src/helpers/ci/index.js: -------------------------------------------------------------------------------- 1 | const { GitHubCI } = require('./github'); 2 | const { GitLabCI } = require('./gitlab'); 3 | const { JenkinsCI } = require('./jenkins'); 4 | const { AzureDevOpsCI } = require('./azure-devops'); 5 | const { CircleCI } = require('./circle-ci'); 6 | const { BaseCI } = require('./base.ci'); 7 | 8 | const ENV = process.env; 9 | 10 | /** 11 | * @returns {import('../../extensions/extensions').ICIInfo} 12 | */ 13 | function getCIInformation() { 14 | if (ENV.GITHUB_ACTIONS) { 15 | const ci = new GitHubCI(); 16 | return ci.info(); 17 | } 18 | 19 | if (ENV.GITLAB_CI) { 20 | const ci = new GitLabCI(); 21 | return ci.info(); 22 | } 23 | 24 | if (ENV.JENKINS_URL) { 25 | const ci = new JenkinsCI(); 26 | return ci.info(); 27 | } 28 | 29 | if (ENV.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { 30 | const ci = new AzureDevOpsCI(); 31 | return ci.info(); 32 | } 33 | 34 | if (ENV.CIRCLECI) { 35 | const ci = new CircleCI(); 36 | return ci.info(); 37 | } 38 | 39 | const ci = new BaseCI(); 40 | return ci.info(); 41 | } 42 | 43 | module.exports = { 44 | getCIInformation 45 | } -------------------------------------------------------------------------------- /src/helpers/ci/jenkins.js: -------------------------------------------------------------------------------- 1 | const { BaseCI } = require('./base.ci'); 2 | 3 | const ENV = process.env; 4 | 5 | class JenkinsCI extends BaseCI { 6 | constructor() { 7 | super(); 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.setInfo(this.targets.ci, 'JENKINS'); 13 | this.setInfo(this.targets.git, ''); 14 | this.setInfo(this.targets.repository_url, ENV.GIT_URL || ENV.GITHUB_URL || ENV.BITBUCKET_URL); 15 | this.setInfo(this.targets.repository_name, ENV.JOB_NAME); 16 | this.setInfo(this.targets.repository_ref, ENV.BRANCH || ENV.BRANCH_NAME); 17 | this.setInfo(this.targets.repository_commit_sha, ENV.GIT_COMMIT || ENV.GIT_COMMIT_SHA || ENV.GITHUB_SHA || ENV.BITBUCKET_COMMIT); 18 | this.setInfo(this.targets.branch_url, this.repository_url + this.repository_ref.replace('refs/heads/', '/tree/')); 19 | this.setInfo(this.targets.branch_name, this.repository_ref.replace('refs/heads/', '')); 20 | 21 | if (this.repository_ref.includes('refs/pull')) { 22 | this.setInfo(this.targets.pull_request_url, this.repository_url + this.repository_ref.replace('refs/pull/', '/pull/')); 23 | this.setInfo(this.targets.pull_request_name, this.repository_ref.replace('refs/pull/', '').replace('/merge', '')); 24 | } 25 | 26 | this.setInfo(this.targets.build_url, ENV.BUILD_URL); 27 | this.setInfo(this.targets.build_number, ENV.BUILD_NUMBER); 28 | this.setInfo(this.targets.build_name, ENV.JOB_NAME); 29 | this.setInfo(this.targets.build_reason, ENV.BUILD_CAUSE); 30 | this.setInfo(this.targets.user, ENV.USER || ENV.USERNAME, true); 31 | } 32 | } 33 | 34 | module.exports = { 35 | JenkinsCI 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/constants.js: -------------------------------------------------------------------------------- 1 | const STATUS = Object.freeze({ 2 | PASS: 'pass', 3 | FAIL: 'fail', 4 | PASS_OR_FAIL: 'passOrfail' 5 | }); 6 | 7 | const HOOK = Object.freeze({ 8 | START: 'start', 9 | AFTER_SUMMARY: 'after-summary', 10 | END: 'end', 11 | }); 12 | 13 | const TARGET = Object.freeze({ 14 | SLACK: 'slack', 15 | TEAMS: 'teams', 16 | CHAT: 'chat', 17 | CUSTOM: 'custom', 18 | DELAY: 'delay', 19 | INFLUX: 'influx', 20 | }); 21 | 22 | const EXTENSION = Object.freeze({ 23 | AI_FAILURE_SUMMARY: 'ai-failure-summary', 24 | FAILURE_ANALYSIS: 'failure-analysis', 25 | SMART_ANALYSIS: 'smart-analysis', 26 | ERROR_CLUSTERS: 'error-clusters', 27 | BROWSERSTACK: 'browserstack', 28 | HYPERLINKS: 'hyperlinks', 29 | MENTIONS: 'mentions', 30 | REPORT_PORTAL_ANALYSIS: 'report-portal-analysis', 31 | REPORT_PORTAL_HISTORY: 'report-portal-history', 32 | QUICK_CHART_TEST_SUMMARY: 'quick-chart-test-summary', 33 | PERCY_ANALYSIS: 'percy-analysis', 34 | CUSTOM: 'custom', 35 | METADATA: 'metadata', 36 | CI_INFO: 'ci-info', 37 | }); 38 | 39 | const URLS = Object.freeze({ 40 | PERCY: 'https://percy.io', 41 | QUICK_CHART: 'https://quickchart.io', 42 | BROWSERSTACK: 'https://api.browserstack.com' 43 | }); 44 | 45 | const PROCESS_STATUS = Object.freeze({ 46 | RUNNING: 'RUNNING', 47 | COMPLETED: 'COMPLETED', 48 | FAILED: 'FAILED', 49 | SKIPPED: 'SKIPPED', 50 | }); 51 | 52 | const MIN_NODE_VERSION = 14; 53 | 54 | module.exports = Object.freeze({ 55 | STATUS, 56 | HOOK, 57 | TARGET, 58 | EXTENSION, 59 | URLS, 60 | PROCESS_STATUS, 61 | MIN_NODE_VERSION 62 | }); -------------------------------------------------------------------------------- /src/helpers/extension.helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add Slack Extension function. 3 | * 4 | * @param {object} param0 - the payload object 5 | * @param {object} param0.payload - the payload object 6 | * @param {import("..").IExtension} param0.extension - the extension to add 7 | * @param {string} param0.text - the text to include 8 | * @return {void} 9 | */ 10 | function addSlackExtension({ payload, extension, text }) { 11 | if (extension.inputs.separator) { 12 | payload.blocks.push({ 13 | "type": "divider" 14 | }); 15 | } 16 | let updated_text = text; 17 | if (extension.inputs.title) { 18 | const title = extension.inputs.title_link ? `<${extension.inputs.title_link}|${extension.inputs.title}>` : extension.inputs.title; 19 | updated_text = `*${title}*\n\n${text}`; 20 | } 21 | if (extension.inputs.block_type === 'context') { 22 | payload.blocks.push({ 23 | "type": "context", 24 | "elements": [ 25 | { 26 | "type": "mrkdwn", 27 | "text": updated_text 28 | } 29 | ] 30 | }); 31 | } else { 32 | payload.blocks.push({ 33 | "type": "section", 34 | "text": { 35 | "type": "mrkdwn", 36 | "text": updated_text 37 | } 38 | }); 39 | } 40 | } 41 | 42 | /** 43 | * Add Teams Extension function. 44 | * 45 | * @param {object} param0 - the payload object 46 | * @param {object} param0.payload - the payload object 47 | * @param {import("..").IExtension} param0.extension - the extension to add 48 | * @param {string} param0.text - the text to include 49 | * @return {void} 50 | */ 51 | function addTeamsExtension({ payload, extension, text }) { 52 | if (extension.inputs.title) { 53 | const title = extension.inputs.title_link ? `[${extension.inputs.title}](${extension.inputs.title_link})` : extension.inputs.title 54 | payload.body.push({ 55 | "type": "TextBlock", 56 | "text": title, 57 | "isSubtle": true, 58 | "weight": "bolder", 59 | "separator": extension.inputs.separator, 60 | "wrap": true 61 | }); 62 | payload.body.push({ 63 | "type": "TextBlock", 64 | "text": text, 65 | "wrap": true 66 | }); 67 | } else { 68 | payload.body.push({ 69 | "type": "TextBlock", 70 | "text": text, 71 | "wrap": true, 72 | "separator": extension.inputs.separator 73 | }); 74 | } 75 | } 76 | 77 | /** 78 | * Add Chat Extension function. 79 | * 80 | * @param {object} param0 - the payload object 81 | * @param {object} param0.payload - the payload object 82 | * @param {import("..").IExtension} param0.extension - the extension to add 83 | * @param {string} param0.text - the text to include 84 | * @return {void} 85 | */ 86 | function addChatExtension({ payload, extension, text }) { 87 | let updated_text = text; 88 | if (extension.inputs.title) { 89 | const title = extension.inputs.title_link ? `${extension.inputs.title}` : extension.inputs.title; 90 | updated_text = `${title}

${text}`; 91 | } 92 | payload.sections.push({ 93 | "widgets": [ 94 | { 95 | "textParagraph": { 96 | "text": updated_text 97 | } 98 | } 99 | ] 100 | }); 101 | } 102 | 103 | module.exports = { 104 | addSlackExtension, 105 | addTeamsExtension, 106 | addChatExtension 107 | } -------------------------------------------------------------------------------- /src/helpers/helper.js: -------------------------------------------------------------------------------- 1 | const pretty_ms = require('pretty-ms'); 2 | 3 | const DATA_REF_PATTERN = /(\{[^\}]+\})/g; 4 | const ALLOWED_CONDITIONS = new Set(['pass', 'fail', 'passorfail']); 5 | const GENERIC_CONDITIONS = new Set(['always', 'never']); 6 | 7 | function getPercentage(x, y) { 8 | if (y > 0) { 9 | return Math.floor((x / y) * 100); 10 | } 11 | return 0; 12 | } 13 | 14 | function processText(raw) { 15 | const dataRefMatches = raw.match(DATA_REF_PATTERN); 16 | if (dataRefMatches) { 17 | for (let i = 0; i < dataRefMatches.length; i++) { 18 | const dataRefMatch = dataRefMatches[i]; 19 | const content = dataRefMatch.slice(1, -1); 20 | const envValue = process.env[content] || content; 21 | raw = raw.replace(dataRefMatch, envValue); 22 | } 23 | } 24 | return raw; 25 | } 26 | 27 | /** 28 | * @returns {import('../index').PublishConfig } 29 | */ 30 | function processData(data) { 31 | if (typeof data === 'string') { 32 | return processText(data); 33 | } 34 | if (typeof data === 'object') { 35 | for (const prop in data) { 36 | data[prop] = processData(data[prop]); 37 | } 38 | } 39 | return data; 40 | } 41 | 42 | /** 43 | * 44 | * @param {string} text 45 | * @param {number} length 46 | * @returns 47 | */ 48 | function truncate(text, length) { 49 | if (text && text.length > length) { 50 | return text.slice(0, length).trim() + "..."; 51 | } else { 52 | return text; 53 | } 54 | } 55 | 56 | function getPrettyDuration(ms, format) { 57 | return pretty_ms(parseInt(ms), { [format]: true, secondsDecimalDigits: 0 }) 58 | } 59 | 60 | function getTitleText({ result, target }) { 61 | const title = target.inputs.title ? target.inputs.title : result.name; 62 | if (target.inputs.title_suffix) { 63 | return `${title} ${target.inputs.title_suffix}`; 64 | } 65 | return `${title}`; 66 | } 67 | 68 | function getResultText({ result }) { 69 | const percentage = getPercentage(result.passed, result.total); 70 | return `${result.passed} / ${result.total} Passed (${percentage}%)`; 71 | } 72 | 73 | /** 74 | * 75 | * @param {object} param0 76 | * @param {string | Function} param0.condition 77 | */ 78 | async function checkCondition({ condition, result, target, extension }) { 79 | if (typeof condition === 'function') { 80 | return await condition({ target, result, extension }); 81 | } else { 82 | const lower_condition = condition.toLowerCase(); 83 | if (ALLOWED_CONDITIONS.has(lower_condition)) { 84 | return lower_condition.includes(result.status.toLowerCase()); 85 | } else if (GENERIC_CONDITIONS.has(lower_condition)) { 86 | return lower_condition === 'always'; 87 | } else { 88 | return eval(condition); 89 | } 90 | } 91 | } 92 | 93 | module.exports = { 94 | getPercentage, 95 | processData, 96 | truncate, 97 | getPrettyDuration, 98 | getTitleText, 99 | getResultText, 100 | checkCondition, 101 | } -------------------------------------------------------------------------------- /src/helpers/metadata.helper.js: -------------------------------------------------------------------------------- 1 | const { checkCondition } = require('./helper'); 2 | 3 | function getMetaDataText(params) { 4 | switch (params.target.name) { 5 | case 'teams': 6 | return getTeamsMetaDataText(params); 7 | case 'slack': 8 | return getSlackMetaDataText(params); 9 | case 'chat': 10 | return getChatMetaDataText(params); 11 | default: 12 | return ''; 13 | } 14 | } 15 | 16 | /** 17 | * Asynchronously generates metadata text for slack. 18 | * 19 | * @param {object} param0 - the payload object 20 | * @param {Object} param0.elements - The elements to generate metadata text from 21 | * @param {import('..').ITarget} param0.target - The result object 22 | * @param {import('..').IExtension} param0.extension - The result object 23 | * @param {Object} param0.result - The result object 24 | * @param {string} param0.default_condition - The default condition object 25 | * @return {string} The generated metadata text 26 | */ 27 | async function getSlackMetaDataText({ elements, target, extension, result, default_condition }) { 28 | const items = []; 29 | for (const element of elements) { 30 | if (await is_valid({ element, result, default_condition })) { 31 | if (element.type === 'hyperlink') { 32 | const url = await get_url({ url: element.value, target, result, extension }); 33 | if (element.label) { 34 | items.push(`*${element.label}:* <${url}|${element.key}>`); 35 | } else { 36 | items.push(`<${url}|${element.key}>`); 37 | } 38 | } else if (element.key) { 39 | items.push(`*${element.key}:* ${element.value}`); 40 | } else { 41 | items.push(element.value); 42 | } 43 | } 44 | } 45 | return items.join(' | '); 46 | } 47 | 48 | /** 49 | * Asynchronously generates metadata text for teams. 50 | * 51 | * @param {object} param0 - the payload object 52 | * @param {Object} param0.elements - The elements to generate metadata text from 53 | * @param {import('..').ITarget} param0.target - The result object 54 | * @param {import('..').IExtension} param0.extension - The result object 55 | * @param {Object} param0.result - The result object 56 | * @param {string} param0.default_condition - The default condition object 57 | * @return {string} The generated metadata text 58 | */ 59 | async function getTeamsMetaDataText({ elements, target, extension, result, default_condition }) { 60 | const items = []; 61 | for (const element of elements) { 62 | if (await is_valid({ element, result, default_condition })) { 63 | if (element.type === 'hyperlink') { 64 | const url = await get_url({ url: element.value, target, result, extension }); 65 | if (element.label) { 66 | items.push(`**${element.label}:** [${element.key}](${url})`); 67 | } else { 68 | items.push(`[${element.key}](${url})`); 69 | } 70 | } else if (element.key) { 71 | items.push(`**${element.key}:** ${element.value}`); 72 | } else { 73 | items.push(element.value); 74 | } 75 | } 76 | } 77 | return items.join(' | '); 78 | } 79 | 80 | /** 81 | * Asynchronously generates metadata text for chat. 82 | * 83 | * @param {object} param0 - the payload object 84 | * @param {Object} param0.elements - The elements to generate metadata text from 85 | * @param {import('..').ITarget} param0.target - The result object 86 | * @param {import('..').IExtension} param0.extension - The result object 87 | * @param {Object} param0.result - The result object 88 | * @param {string} param0.default_condition - The default condition object 89 | * @return {string} The generated metadata text 90 | */ 91 | async function getChatMetaDataText({ elements, target, extension, result, default_condition }) { 92 | const items = []; 93 | for (const element of elements) { 94 | if (await is_valid({ element, result, default_condition })) { 95 | if (element.type === 'hyperlink') { 96 | const url = await get_url({ url: element.value, target, result, extension }); 97 | if (element.label) { 98 | items.push(`${element.label}: ${element.key}`); 99 | } else { 100 | items.push(`${element.key}`); 101 | } 102 | } else if (element.key) { 103 | items.push(`${element.key}: ${element.value}`); 104 | } else { 105 | items.push(element.value); 106 | } 107 | } 108 | } 109 | return items.join(' | '); 110 | } 111 | 112 | function is_valid({ element, result, default_condition }) { 113 | const condition = element.condition || default_condition; 114 | return checkCondition({ condition, result }); 115 | } 116 | 117 | function get_url({ url, target, extension, result}) { 118 | if (typeof url === 'function') { 119 | return url({target, extension, result}); 120 | } 121 | return url; 122 | } 123 | 124 | module.exports = { 125 | getMetaDataText, 126 | getSlackMetaDataText, 127 | getTeamsMetaDataText, 128 | getChatMetaDataText 129 | } -------------------------------------------------------------------------------- /src/helpers/percy.js: -------------------------------------------------------------------------------- 1 | const request = require('phin-retry'); 2 | 3 | /** 4 | * @param {import('../index').PercyAnalysisInputs} inputs 5 | */ 6 | async function getProjectByName(inputs) { 7 | return request.get({ 8 | url: `${inputs.url}/api/v1/projects`, 9 | headers: { 10 | 'Authorization': `Token ${inputs.token}` 11 | }, 12 | form: { 13 | 'project_slug': inputs.project_name 14 | } 15 | }); 16 | } 17 | 18 | /** 19 | * @param {import('../index').PercyAnalysisInputs} inputs 20 | */ 21 | async function getLastBuild(inputs) { 22 | return request.get({ 23 | url: `${inputs.url}/api/v1/builds?project_id=${inputs.project_id}&page[limit]=1`, 24 | headers: { 25 | 'Authorization': `Token ${inputs.token}` 26 | } 27 | }); 28 | } 29 | 30 | /** 31 | * @param {import('../index').PercyAnalysisInputs} inputs 32 | */ 33 | async function getBuild(inputs) { 34 | return request.get({ 35 | url: `${inputs.url}/api/v1/builds/${inputs.build_id}`, 36 | headers: { 37 | 'Authorization': `Token ${inputs.token}` 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * @param {import('../index').PercyAnalysisInputs} inputs 44 | */ 45 | async function getRemovedSnapshots(inputs) { 46 | return request.get({ 47 | url: `${inputs.url}/api/v1/builds/${inputs.build_id}/removed-snapshots`, 48 | headers: { 49 | 'Authorization': `Token ${inputs.token}` 50 | } 51 | }); 52 | } 53 | 54 | 55 | module.exports = { 56 | getProjectByName, 57 | getLastBuild, 58 | getBuild, 59 | getRemovedSnapshots 60 | } -------------------------------------------------------------------------------- /src/helpers/performance.js: -------------------------------------------------------------------------------- 1 | const Metric = require("performance-results-parser/src/models/Metric"); 2 | const { checkCondition } = require("./helper"); 3 | const pretty_ms = require('pretty-ms'); 4 | 5 | /** 6 | * @param {object} param0 7 | * @param {Metric[]} param0.metrics 8 | * @param {object} param0.result 9 | * @param {object} param0.target 10 | */ 11 | async function getValidMetrics({ metrics, result, target }) { 12 | if (target.inputs.metrics && target.inputs.metrics.length > 0) { 13 | const valid_metrics = []; 14 | for (let i = 0; i < metrics.length; i++) { 15 | const metric = metrics[i]; 16 | for (let j = 0; j < target.inputs.metrics.length; j++) { 17 | const metric_config = target.inputs.metrics[j]; 18 | if (metric.name === metric_config.name) { 19 | const include = await checkCondition({ condition: metric_config.condition || 'always', result, target }); 20 | if (include) valid_metrics.push(metric); 21 | } 22 | } 23 | } 24 | return valid_metrics; 25 | } 26 | return metrics; 27 | } 28 | 29 | /** 30 | * @param {Metric} metric 31 | * @param {string[]} fields 32 | */ 33 | function getCounterMetricFieldValue(metric, fields) { 34 | let value = ''; 35 | if (fields.includes('sum')) { 36 | const sum_failure = metric.failures.find(_failure => _failure.field === 'sum'); 37 | if (sum_failure) { 38 | const emoji = getEmoji(sum_failure.difference); 39 | value = `${emoji} ${metric['sum']} (${getDifferenceSymbol(sum_failure.difference)}${sum_failure.difference}) ` 40 | } else { 41 | value = `${metric['sum']} `; 42 | } 43 | } 44 | if (fields.includes('rate')) { 45 | let metric_unit = metric.unit.startsWith('/') ? metric.unit : ` ${metric.unit}`; 46 | const rate_failure = metric.failures.find(_failure => _failure.field === 'rate'); 47 | if (rate_failure) { 48 | const emoji = getEmoji(rate_failure.difference); 49 | value += `${emoji} ${metric['rate']}${metric_unit} (${getDifferenceSymbol(rate_failure.difference)}${rate_failure.difference})` 50 | } else { 51 | value += `${metric['rate']}${metric_unit}`; 52 | } 53 | } 54 | return value; 55 | } 56 | 57 | /** 58 | * @param {Metric} metric 59 | */ 60 | function getTrendMetricFieldValue(metric, field) { 61 | const failure = metric.failures.find(_failure => _failure.field === field); 62 | if (failure) { 63 | const emoji = getEmoji(failure.difference); 64 | return `${emoji} ${field}=${pretty_ms(metric[field])} (${getDifferenceSymbol(failure.difference)}${pretty_ms(failure.difference)})` 65 | } 66 | return `${field}=${pretty_ms(metric[field])}`; 67 | } 68 | 69 | /** 70 | * @param {Metric} metric 71 | */ 72 | function getRateMetricFieldValue(metric) { 73 | const failure = metric.failures.find(_failure => _failure.field === 'rate'); 74 | if (failure) { 75 | const emoji = getEmoji(failure.difference); 76 | return `${emoji} ${metric['rate']} ${metric.unit} (${getDifferenceSymbol(failure.difference)}${failure.difference})`; 77 | } 78 | return `${metric['rate']} ${metric.unit}`; 79 | } 80 | 81 | function getEmoji(value) { 82 | return value > 0 ? '🔺' : '🔻'; 83 | } 84 | 85 | function getDifferenceSymbol(value) { 86 | return value > 0 ? `+` : ''; 87 | } 88 | 89 | /** 90 | * 91 | * @param {object} param0 92 | * @param {Metric} param0.metric 93 | */ 94 | function getDisplayFields({ metric, target }) { 95 | let fields = []; 96 | if (target.inputs.metrics) { 97 | const metric_config = target.inputs.metrics.find(_metric => _metric.name === metric.name); 98 | if (metric_config) { 99 | fields = metric_config.fields; 100 | } 101 | } 102 | if (fields && fields.length > 0) { 103 | return fields; 104 | } else { 105 | switch (metric.type) { 106 | case 'COUNTER': 107 | return ['sum', 'rate']; 108 | case 'RATE': 109 | return ['rate']; 110 | case 'TREND': 111 | return ['avg', 'min', 'med', 'max', 'p90', 'p95', 'p99']; 112 | default: 113 | return ['sum', 'min', 'max']; 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * @param {object} param0 120 | * @param {Metric} param0.metric 121 | */ 122 | function getMetricValuesText({ metric, target }) { 123 | const fields = getDisplayFields({ metric, target }); 124 | const values = []; 125 | if (metric.type === 'COUNTER') { 126 | values.push(getCounterMetricFieldValue(metric, fields)); 127 | } else if (metric.type === 'TREND') { 128 | for (let i = 0; i < fields.length; i++) { 129 | const field_metric = fields[i]; 130 | values.push(getTrendMetricFieldValue(metric, field_metric)); 131 | } 132 | } else if (metric.type === 'RATE') { 133 | values.push(getRateMetricFieldValue(metric)); 134 | } 135 | return values.join(' | '); 136 | } 137 | 138 | module.exports = { 139 | getValidMetrics, 140 | getMetricValuesText 141 | } -------------------------------------------------------------------------------- /src/helpers/report-portal.js: -------------------------------------------------------------------------------- 1 | const request = require('phin-retry'); 2 | 3 | async function getLaunchDetails(options) { 4 | return request.get({ 5 | url: `${options.url}/api/v1/${options.project}/launch/${options.launch_id}`, 6 | headers: { 7 | 'Authorization': `Bearer ${options.api_key}` 8 | } 9 | }); 10 | } 11 | 12 | async function getLaunchesByName(options) { 13 | return request.get({ 14 | url: `${options.url}/api/v1/${options.project}/launch?filter.eq.name=${options.launch_name}&page.size=1&page.sort=startTime%2Cdesc`, 15 | headers: { 16 | 'Authorization': `Bearer ${options.api_key}` 17 | } 18 | }); 19 | } 20 | 21 | async function getLastLaunchByName(options) { 22 | const response = await getLaunchesByName(options); 23 | if (response.content && response.content.length > 0) { 24 | return response.content[0]; 25 | } 26 | return null; 27 | } 28 | 29 | async function getSuiteHistory(options) { 30 | return request.get({ 31 | url: `${options.url}/api/v1/${options.project}/item/history`, 32 | qs: { 33 | historyDepth: options.history_depth, 34 | 'filter.eq.launchId': options.launch_id, 35 | 'filter.!ex.parentId': 'true' 36 | }, 37 | headers: { 38 | 'Authorization': `Bearer ${options.api_key}` 39 | } 40 | }); 41 | } 42 | 43 | module.exports = { 44 | getLaunchDetails, 45 | getLastLaunchByName, 46 | getSuiteHistory 47 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { PublishCommand } = require('./commands/publish.command'); 2 | const { GenerateConfigCommand } = require('./commands/generate-config.command'); 3 | 4 | function publish(options) { 5 | const publish_command = new PublishCommand(options); 6 | return publish_command.publish(); 7 | } 8 | 9 | function generateConfig() { 10 | const generate_command = new GenerateConfigCommand(); 11 | return generate_command.execute(); 12 | } 13 | 14 | function defineConfig(config) { 15 | return config; 16 | } 17 | 18 | module.exports = { 19 | publish, 20 | generateConfig, 21 | defineConfig 22 | } -------------------------------------------------------------------------------- /src/platforms/base.platform.js: -------------------------------------------------------------------------------- 1 | const { getPercentage, getPrettyDuration } = require('../helpers/helper') 2 | 3 | class BasePlatform { 4 | 5 | /** 6 | * @param {string|number} text 7 | */ 8 | bold(text) { 9 | throw new Error('Not Implemented'); 10 | } 11 | 12 | break() { 13 | throw new Error('Not Implemented'); 14 | } 15 | 16 | /** 17 | * 18 | * @param {import('..').ITarget} target 19 | * @param {import('test-results-parser').ITestSuite} suite 20 | */ 21 | getSuiteSummaryText(target, suite) { 22 | const suite_title = this.getSuiteTitle(suite); 23 | const suite_results_text = this.#getSuiteResultsText(suite); 24 | const duration_text = this.#getSuiteDurationText(target, suite); 25 | 26 | const texts = [ 27 | this.bold(suite_title), 28 | this.break(), 29 | this.break(), 30 | suite_results_text, 31 | this.break(), 32 | duration_text, 33 | ]; 34 | 35 | const metadata_text = this.getSuiteMetaDataText(suite); 36 | 37 | if (metadata_text) { 38 | texts.push(this.break()); 39 | texts.push(this.break()); 40 | texts.push(metadata_text); 41 | } 42 | 43 | return texts.join(''); 44 | } 45 | 46 | /** 47 | * 48 | * @param {import('test-results-parser').ITestSuite} suite 49 | * @returns {string} 50 | */ 51 | getSuiteTitle(suite) { 52 | const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌'; 53 | return `${emoji} ${suite.name}`; 54 | } 55 | 56 | /** 57 | * 58 | * @param {import('test-results-parser').ITestSuite} suite 59 | * @returns {string} 60 | */ 61 | #getSuiteResultsText(suite) { 62 | const suite_results = this.getSuiteResults(suite); 63 | return `${this.bold('Results')}: ${suite_results}`; 64 | } 65 | 66 | /** 67 | * 68 | * @param {import('test-results-parser').ITestSuite} suite 69 | * @returns {string} 70 | */ 71 | getSuiteResults(suite) { 72 | return `${suite.passed} / ${suite.total} Passed (${getPercentage(suite.passed, suite.total)}%)`; 73 | } 74 | 75 | /** 76 | * 77 | * @param {import('..').ITarget} target 78 | * @param {import('test-results-parser').ITestSuite} suite 79 | */ 80 | #getSuiteDurationText(target, suite) { 81 | const duration = this.getSuiteDuration(target, suite); 82 | return `${this.bold('Duration')}: ${duration}` 83 | } 84 | 85 | /** 86 | * 87 | * @param {import('..').ITarget} target 88 | * @param {import('test-results-parser').ITestSuite} suite 89 | */ 90 | getSuiteDuration(target, suite) { 91 | return getPrettyDuration(suite.duration, target.inputs.duration); 92 | } 93 | 94 | /** 95 | * 96 | * @param {import('test-results-parser').ITestSuite} suite 97 | * @returns {string} 98 | */ 99 | getSuiteMetaDataText(suite) { 100 | if (!suite || !suite.metadata) { 101 | return; 102 | } 103 | 104 | const texts = []; 105 | 106 | // webdriver io 107 | if (suite.metadata.device && typeof suite.metadata.device === 'string') { 108 | texts.push(`${suite.metadata.device}`); 109 | } 110 | 111 | if (suite.metadata.platform && suite.metadata.platform.name && suite.metadata.platform.version) { 112 | texts.push(`${suite.metadata.platform.name} ${suite.metadata.platform.version}`); 113 | } 114 | 115 | if (suite.metadata.browser && suite.metadata.browser.name && suite.metadata.browser.version) { 116 | texts.push(`${suite.metadata.browser.name} ${suite.metadata.browser.version}`); 117 | } 118 | 119 | // playwright 120 | if (suite.metadata.hostname && typeof suite.metadata.hostname === 'string') { 121 | texts.push(`${suite.metadata.hostname}`); 122 | } 123 | 124 | return texts.join(' • '); 125 | } 126 | } 127 | 128 | module.exports = { BasePlatform } -------------------------------------------------------------------------------- /src/platforms/chat.platform.js: -------------------------------------------------------------------------------- 1 | const { BasePlatform } = require("./base.platform"); 2 | 3 | class ChatPlatform extends BasePlatform { 4 | 5 | /** 6 | * @param {string|number} text 7 | */ 8 | bold(text) { 9 | return `${text}`; 10 | } 11 | 12 | break() { 13 | return '
'; 14 | } 15 | 16 | } 17 | 18 | module.exports = { ChatPlatform } -------------------------------------------------------------------------------- /src/platforms/index.js: -------------------------------------------------------------------------------- 1 | const { TARGET } = require("../helpers/constants"); 2 | const { SlackPlatform } = require('./slack.platform'); 3 | const { TeamsPlatform } = require('./teams.platform'); 4 | const { ChatPlatform } = require('./chat.platform'); 5 | 6 | /** 7 | * 8 | * @param {string} name 9 | */ 10 | function getPlatform(name) { 11 | switch (name) { 12 | case TARGET.SLACK: 13 | return new SlackPlatform(); 14 | case TARGET.TEAMS: 15 | return new TeamsPlatform(); 16 | case TARGET.CHAT: 17 | return new ChatPlatform(); 18 | default: 19 | throw new Error('Invalid Platform'); 20 | } 21 | } 22 | 23 | module.exports = { 24 | getPlatform 25 | } -------------------------------------------------------------------------------- /src/platforms/slack.platform.js: -------------------------------------------------------------------------------- 1 | const { BasePlatform } = require("./base.platform"); 2 | 3 | class SlackPlatform extends BasePlatform { 4 | 5 | /** 6 | * @param {string|number} text 7 | */ 8 | bold(text) { 9 | return `*${text}*`; 10 | } 11 | 12 | break() { 13 | return '\n'; 14 | } 15 | } 16 | 17 | module.exports = { SlackPlatform } -------------------------------------------------------------------------------- /src/platforms/teams.platform.js: -------------------------------------------------------------------------------- 1 | const { BasePlatform } = require("./base.platform"); 2 | 3 | class TeamsPlatform extends BasePlatform { 4 | /** 5 | * @param {string|number} text 6 | */ 7 | bold(text) { 8 | return `**${text}**`; 9 | } 10 | 11 | break() { 12 | return '\n\n'; 13 | } 14 | } 15 | 16 | module.exports = { TeamsPlatform } -------------------------------------------------------------------------------- /src/setups/extensions.setup.js: -------------------------------------------------------------------------------- 1 | const { getAutomationBuilds, getAutomationBuildSessions } = require('../helpers/browserstack.helper'); 2 | 3 | class ExtensionsSetup { 4 | constructor(extensions, result) { 5 | this.extensions = extensions; 6 | this.result = result; 7 | } 8 | 9 | async run() { 10 | for (const extension of this.extensions) { 11 | if (extension.name === 'browserstack') { 12 | const browserStackExtensionSetup = new BrowserStackExtensionSetup(extension.inputs, this.result); 13 | await browserStackExtensionSetup.setup(); 14 | } 15 | } 16 | } 17 | } 18 | 19 | class BrowserStackExtensionSetup { 20 | constructor(inputs, result) { 21 | /** @type {import('../index').BrowserstackInputs} */ 22 | this.inputs = inputs; 23 | this.result = result; 24 | } 25 | 26 | async setup() { 27 | const build_rows = await getAutomationBuilds(this.inputs); 28 | if (!Array.isArray(build_rows) || build_rows.length === 0) { 29 | throw new Error('No builds found'); 30 | } 31 | const automation_build = build_rows.find(_ => _.automation_build.name === this.inputs.automation_build_name); 32 | if (!automation_build) { 33 | throw new Error(`Build ${this.inputs.automation_build_name} not found`); 34 | } 35 | this.inputs.automation_build = automation_build.automation_build; 36 | if (this.result) { 37 | this.result.metadata = this.result.metadata || {}; 38 | this.result.metadata.ext_browserstack_automation_build = automation_build.automation_build; 39 | } 40 | this.inputs.automation_sessions = []; 41 | const session_rows = await getAutomationBuildSessions(this.inputs, automation_build.automation_build.hashed_id); 42 | if (!Array.isArray(session_rows) || session_rows.length === 0) { 43 | throw new Error('No sessions found'); 44 | } 45 | for (const session_row of session_rows) { 46 | this.inputs.automation_sessions.push(session_row.automation_session); 47 | } 48 | if (this.result && this.result.suites && this.result.suites.length > 0) { 49 | for (const suite of this.result.suites) { 50 | const automation_session = this.inputs.automation_sessions.filter(_ => { _.name === suite.name })[0]; 51 | if (automation_session) { 52 | suite.metadata = suite.metadata || {}; 53 | suite.metadata.ext_browserstack_automation_session = automation_session; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | module.exports = { 61 | ExtensionsSetup 62 | } -------------------------------------------------------------------------------- /src/targets/custom.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { STATUS } = require('../helpers/constants'); 3 | 4 | /** 5 | * 6 | * @param {object} param0 7 | * @param {import('../index').ITarget} param0.target 8 | */ 9 | async function run({result, target}) { 10 | if (typeof target.inputs.load === 'string') { 11 | const cwd = process.cwd(); 12 | const target_runner = require(path.join(cwd, target.inputs.load)); 13 | await target_runner.run({ target, result }); 14 | } else if (typeof target.inputs.load === 'function') { 15 | await target.inputs.load({ target, result }); 16 | } else { 17 | throw `Invalid 'load' input in custom target - ${target.inputs.load}`; 18 | } 19 | } 20 | 21 | const default_options = { 22 | condition: STATUS.PASS_OR_FAIL 23 | } 24 | 25 | module.exports = { 26 | run, 27 | default_options 28 | } -------------------------------------------------------------------------------- /src/targets/delay.js: -------------------------------------------------------------------------------- 1 | const { STATUS } = require("../helpers/constants"); 2 | 3 | async function run({ target }) { 4 | target.inputs = Object.assign({}, default_inputs, target.inputs); 5 | await new Promise(resolve => setTimeout(resolve, target.inputs.seconds * 1000)); 6 | } 7 | 8 | const default_options = { 9 | condition: STATUS.PASS_OR_FAIL 10 | } 11 | 12 | const default_inputs = { 13 | seconds: 5 14 | } 15 | 16 | module.exports = { 17 | run, 18 | default_options 19 | } -------------------------------------------------------------------------------- /src/targets/index.js: -------------------------------------------------------------------------------- 1 | const teams = require('./teams'); 2 | const slack = require('./slack'); 3 | const chat = require('./chat'); 4 | const custom = require('./custom'); 5 | const delay = require('./delay'); 6 | const influx = require('./influx'); 7 | const { TARGET } = require('../helpers/constants'); 8 | const { checkCondition } = require('../helpers/helper'); 9 | 10 | function getTargetRunner(target) { 11 | switch (target.name) { 12 | case TARGET.TEAMS: 13 | return teams; 14 | case TARGET.SLACK: 15 | return slack; 16 | case TARGET.CHAT: 17 | return chat; 18 | case TARGET.CUSTOM: 19 | return custom; 20 | case TARGET.DELAY: 21 | return delay; 22 | case TARGET.INFLUX: 23 | return influx; 24 | default: 25 | return require(target.name); 26 | } 27 | } 28 | 29 | async function run(target, result) { 30 | const target_runner = getTargetRunner(target); 31 | const target_options = Object.assign({}, target_runner.default_options, target); 32 | if (await checkCondition({ condition: target_options.condition, result, target })) { 33 | await target_runner.run({result, target}); 34 | } 35 | } 36 | 37 | async function handleErrors({ target, errors }) { 38 | const target_runner = getTargetRunner(target); 39 | if (target_runner.handleErrors) { 40 | await target_runner.handleErrors({ target, errors }); 41 | } 42 | } 43 | 44 | module.exports = { 45 | run, 46 | handleErrors 47 | } -------------------------------------------------------------------------------- /src/utils/config.builder.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const logger = require('./logger'); 3 | 4 | class ConfigBuilder { 5 | 6 | /** 7 | * @param {import('../index').CommandLineOptions} opts 8 | */ 9 | constructor(opts) { 10 | this.opts = opts; 11 | } 12 | 13 | build() { 14 | if (!this.opts) { 15 | return; 16 | } 17 | if (typeof this.opts.config === 'object') { 18 | return 19 | } 20 | if (this.opts.config && typeof this.opts.config === 'string') { 21 | return; 22 | } 23 | 24 | logger.info('🏗 Building config...') 25 | this.#buildConfig(); 26 | this.#buildBeats(); 27 | this.#buildResults(); 28 | this.#buildTargets(); 29 | this.#buildExtensions(); 30 | 31 | logger.debug(`🛠️ Generated Config: \n${JSON.stringify(this.config, null, 2)}`); 32 | 33 | this.opts.config = this.config; 34 | } 35 | 36 | #buildConfig() { 37 | /** @type {import('../index').PublishConfig} */ 38 | this.config = {}; 39 | } 40 | 41 | #buildBeats() { 42 | this.config.project = this.opts.project || this.config.project; 43 | this.config.run = this.opts.run || this.config.run; 44 | this.config.api_key = this.opts['api-key'] || this.config.api_key; 45 | } 46 | 47 | #buildResults() { 48 | if (this.opts.junit) { 49 | this.#addResults('junit', this.opts.junit); 50 | } 51 | if (this.opts.testng) { 52 | this.#addResults('testng', this.opts.testng); 53 | } 54 | if (this.opts.cucumber) { 55 | this.#addResults('cucumber', this.opts.cucumber); 56 | } 57 | if (this.opts.mocha) { 58 | this.#addResults('mocha', this.opts.mocha); 59 | } 60 | if (this.opts.nunit) { 61 | this.#addResults('nunit', this.opts.nunit); 62 | } 63 | if (this.opts.xunit) { 64 | this.#addResults('xunit', this.opts.xunit); 65 | } 66 | if (this.opts.mstest) { 67 | this.#addResults('mstest', this.opts.mstest); 68 | } 69 | } 70 | 71 | /** 72 | * @param {string} type 73 | * @param {string} file 74 | */ 75 | #addResults(type, file) { 76 | this.config.results = [ 77 | { 78 | type, 79 | files: [path.join(file)] 80 | } 81 | ] 82 | } 83 | 84 | #buildTargets() { 85 | if (this.opts.slack) { 86 | this.#addTarget('slack', this.opts.slack); 87 | } 88 | if (this.opts.teams) { 89 | this.#addTarget('teams', this.opts.teams); 90 | } 91 | if (this.opts.chat) { 92 | this.#addTarget('chat', this.opts.chat); 93 | } 94 | } 95 | 96 | #addTarget(name, url) { 97 | this.config.targets = this.config.targets || []; 98 | this.config.targets.push({ name, inputs: { url, title: this.opts.title || '', only_failures: true } }) 99 | } 100 | 101 | #buildExtensions() { 102 | if (this.opts['ci-info']) { 103 | this.#addExtension('ci-info'); 104 | } 105 | if (this.opts['chart-test-summary']) { 106 | this.#addExtension('quick-chart-test-summary'); 107 | } 108 | } 109 | 110 | #addExtension(name) { 111 | this.config.extensions = this.config.extensions || []; 112 | this.config.extensions.push({ name }); 113 | } 114 | 115 | } 116 | 117 | module.exports = { ConfigBuilder }; -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const trm = console; 2 | 3 | const LEVEL_VERBOSE = 2; 4 | const LEVEL_TRACE = 3; 5 | const LEVEL_DEBUG = 4; 6 | const LEVEL_INFO = 5; 7 | const LEVEL_WARN = 6; 8 | const LEVEL_ERROR = 7; 9 | const LEVEL_SILENT = 8; 10 | 11 | /** 12 | * returns log level value 13 | * @param {string} level - log level 14 | */ 15 | function getLevelValue(level) { 16 | const logLevel = level.toUpperCase(); 17 | switch (logLevel) { 18 | case 'TRACE': 19 | return LEVEL_TRACE; 20 | case 'DEBUG': 21 | return LEVEL_DEBUG; 22 | case 'INFO': 23 | return LEVEL_INFO; 24 | case 'WARN': 25 | return LEVEL_WARN; 26 | case 'ERROR': 27 | return LEVEL_ERROR; 28 | case 'SILENT': 29 | return LEVEL_SILENT; 30 | case 'VERBOSE': 31 | return LEVEL_VERBOSE; 32 | default: 33 | return LEVEL_INFO; 34 | } 35 | } 36 | 37 | class Logger { 38 | 39 | constructor() { 40 | this.level = process.env.TESTBEATS_LOG_LEVEL || 'INFO'; 41 | this.levelValue = getLevelValue(this.level); 42 | if (process.env.TESTBEATS_DISABLE_LOG_COLORS === 'true') { 43 | options.disableColors = true; 44 | } 45 | } 46 | 47 | /** 48 | * sets log level 49 | * @param {('TRACE'|'DEBUG'|'INFO'|'WARN'|'ERROR')} level - log level 50 | */ 51 | setLevel(level) { 52 | this.level = level; 53 | this.levelValue = getLevelValue(this.level); 54 | } 55 | 56 | trace(...msg) { 57 | if (this.levelValue <= LEVEL_TRACE) { 58 | msg.forEach(m => trm.debug(m)); 59 | } 60 | } 61 | 62 | debug(...msg) { 63 | if (this.levelValue <= LEVEL_DEBUG) { 64 | msg.forEach(m => trm.debug(m)); 65 | } 66 | } 67 | 68 | info(...msg) { 69 | if (this.levelValue <= LEVEL_INFO) { 70 | msg.forEach(m => trm.info(m)); 71 | } 72 | } 73 | 74 | warn(...msg) { 75 | if (this.levelValue <= LEVEL_WARN) { 76 | msg.forEach(m => trm.warn(getMessage(m))); 77 | } 78 | } 79 | 80 | error(...msg) { 81 | if (this.levelValue <= LEVEL_ERROR) { 82 | msg.forEach(m => trm.error(getMessage(m))); 83 | } 84 | } 85 | 86 | } 87 | 88 | 89 | function getMessage(msg) { 90 | try { 91 | return typeof msg === 'object' ? JSON.stringify(msg, null, 2) : msg; 92 | } catch (_) { 93 | return msg; 94 | } 95 | } 96 | 97 | 98 | module.exports = new Logger(); -------------------------------------------------------------------------------- /test/base.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | 3 | before(async () => { 4 | await mock.start(); 5 | // require('./helpers/interactions'); 6 | require('./mocks'); 7 | process.env.TEST_BEATS_URL = 'http://localhost:9393'; 8 | }); 9 | 10 | after(async () => { 11 | await mock.stop(); 12 | }); -------------------------------------------------------------------------------- /test/ci.spec.js: -------------------------------------------------------------------------------- 1 | const { getCIInformation } = require('../src/helpers/ci'); 2 | const assert = require('assert'); 3 | 4 | describe('CI', () => { 5 | 6 | it('should return default CI information', () => { 7 | const info = getCIInformation(); 8 | 9 | assert.ok(typeof info.runtime === 'string', 'runtime should be a string'); 10 | assert.ok(typeof info.runtime_version === 'string', 'runtime_version should be a string'); 11 | assert.ok(typeof info.os === 'string', 'os should be a string'); 12 | assert.ok(typeof info.os_version === 'string', 'os_version should be a string'); 13 | assert.ok(typeof info.testbeats_version === 'string', 'testbeats_version should be a string'); 14 | assert.ok(typeof info.user === 'string', 'user should be a string'); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /test/cli.spec.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const assert = require('assert'); 3 | const { mock } = require('pactum'); 4 | 5 | describe('CLI', () => { 6 | 7 | it('publish results with config file', (done) => { 8 | mock.addInteraction('post test-summary to slack'); 9 | exec('node src/cli.js publish --config test/data/configs/slack.config.json', (error, stdout, stderr) => { 10 | console.log(stdout); 11 | assert.match(stdout, /✅ Results published successfully!/); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('publish results with alias config param', (done) => { 17 | mock.addInteraction('post test-summary to slack'); 18 | exec('node src/cli.js publish -c test/data/configs/slack.config.json', (error, stdout, stderr) => { 19 | console.log(stdout); 20 | assert.match(stdout, /✅ Results published successfully!/); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('publish results with config builder', (done) => { 26 | mock.addInteraction('post test-summary to slack'); 27 | exec('node src/cli.js publish --slack http://localhost:9393/message --testng test/data/testng/single-suite.xml', (error, stdout, stderr) => { 28 | console.log(stdout); 29 | assert.match(stdout, /✅ Results published successfully!/); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('publish results with config builder and extension', (done) => { 35 | mock.addInteraction('post test-summary to teams with qc-test-summary', { quickChartUrl: "https://quickchart.io" }); 36 | exec('node src/cli.js publish --teams http://localhost:9393/message --testng test/data/testng/single-suite-failures.xml --chart-test-summary', (error, stdout, stderr) => { 37 | console.log(stdout); 38 | assert.match(stdout, /✅ Results published successfully!/); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('publish results to beats', (done) => { 44 | mock.addInteraction('post test results to beats'); 45 | // mock.addInteraction('get test results from beats'); 46 | mock.addInteraction('post test-summary with beats to teams'); 47 | exec('node src/cli.js publish --api-key api-key --project project-name --run build-name --teams http://localhost:9393/message --testng test/data/testng/single-suite.xml', (error, stdout, stderr) => { 48 | console.log(stdout); 49 | assert.match(stdout, /🚀 Publishing results to TestBeats Portal/); 50 | assert.match(stdout, /✅ Results published successfully!/); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('publish results with config file and cli options', (done) => { 56 | mock.addInteraction('post test results to beats'); 57 | mock.addInteraction('get test results from beats'); 58 | mock.addInteraction('post test-summary with beats to teams'); 59 | exec('node src/cli.js publish --api-key api-key --project project-name --run build-name --config test/data/configs/teams.config.json', (error, stdout, stderr) => { 60 | console.log(stdout); 61 | assert.match(stdout, /🚀 Publishing results to TestBeats Portal/); 62 | assert.match(stdout, /✅ Results published successfully!/); 63 | done(); 64 | }); 65 | }); 66 | 67 | });; -------------------------------------------------------------------------------- /test/condition.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('Condition', () => { 6 | 7 | it('custom js expression at target - successful', async () => { 8 | const id = mock.addInteraction('post test-summary to teams'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "teams", 14 | "condition": "result.status === 'PASS'", 15 | "inputs": { 16 | "url": "http://localhost:9393/message" 17 | } 18 | } 19 | ], 20 | "results": [ 21 | { 22 | "type": "testng", 23 | "files": [ 24 | "test/data/testng/single-suite.xml" 25 | ] 26 | } 27 | ] 28 | } 29 | }); 30 | assert.equal(mock.getInteraction(id).exercised, true); 31 | }); 32 | 33 | it('custom js expression at target - failure', async () => { 34 | await publish({ 35 | config: { 36 | "targets": [ 37 | { 38 | "name": "teams", 39 | "condition": "result.status === 'FAIL'", 40 | "inputs": { 41 | "url": "http://localhost:9393/message" 42 | } 43 | } 44 | ], 45 | "results": [ 46 | { 47 | "type": "testng", 48 | "files": [ 49 | "test/data/testng/single-suite.xml" 50 | ] 51 | } 52 | ] 53 | } 54 | }); 55 | }); 56 | 57 | it('custom js expression at extension - successful', async () => { 58 | const id = mock.addInteraction('post test-summary with hyperlinks to teams - pass status'); 59 | await publish({ 60 | config: { 61 | "targets": [ 62 | { 63 | "name": "teams", 64 | "condition": "pass", 65 | "inputs": { 66 | "url": "http://localhost:9393/message" 67 | }, 68 | "extensions": [ 69 | { 70 | "name": "hyperlinks", 71 | "condition": "result.status === 'PASS'", 72 | "inputs": { 73 | "links": [ 74 | { 75 | "text": "Pipeline", 76 | "url": "some-url" 77 | }, 78 | { 79 | "text": "Video", 80 | "url": "some-url" 81 | } 82 | ] 83 | } 84 | } 85 | ] 86 | } 87 | ], 88 | "results": [ 89 | { 90 | "type": "testng", 91 | "files": [ 92 | "test/data/testng/single-suite.xml" 93 | ] 94 | } 95 | ] 96 | } 97 | }); 98 | assert.equal(mock.getInteraction(id).exercised, true); 99 | }); 100 | 101 | it('custom js expression at hyperlink extension - failure', async () => { 102 | const id = mock.addInteraction('post test-summary with hyperlinks to teams - pass status'); 103 | await publish({ 104 | config: { 105 | "targets": [ 106 | { 107 | "name": "teams", 108 | "condition": "pass", 109 | "inputs": { 110 | "url": "http://localhost:9393/message" 111 | }, 112 | "extensions": [ 113 | { 114 | "name": "hyperlinks", 115 | "inputs": { 116 | "links": [ 117 | { 118 | "text": "Pipeline", 119 | "url": "some-url" 120 | }, 121 | { 122 | "text": "Video", 123 | "url": "some-url" 124 | }, 125 | { 126 | "text": "Fake", 127 | "url": "some-url", 128 | "condition": "result.status === 'FAIL'" 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | ], 136 | "results": [ 137 | { 138 | "type": "testng", 139 | "files": [ 140 | "test/data/testng/single-suite.xml" 141 | ] 142 | } 143 | ] 144 | } 145 | }); 146 | assert.equal(mock.getInteraction(id).exercised, true); 147 | }); 148 | 149 | }); -------------------------------------------------------------------------------- /test/data/configs/custom-target.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "name": "custom", 5 | "inputs": { 6 | "load": "test/data/custom/custom-target-runner.js" 7 | } 8 | } 9 | ], 10 | "results": [ 11 | { 12 | "type": "junit", 13 | "files": ["test/data/junit/single-suite.xml"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/data/configs/slack.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "name": "slack", 5 | "inputs": { 6 | "url": "http://localhost:9393/message" 7 | } 8 | } 9 | ], 10 | "results": [ 11 | { 12 | "type": "testng", 13 | "files": [ 14 | "test/data/testng/single-suite.xml" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/data/configs/teams.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "name": "teams", 5 | "inputs": { 6 | "url": "http://localhost:9393/message" 7 | } 8 | } 9 | ], 10 | "results": [ 11 | { 12 | "type": "testng", 13 | "files": [ 14 | "test/data/testng/single-suite.xml" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/data/cucumber/suites-with-metadata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "Verify calculator functionalities", 4 | "elements": [ 5 | { 6 | "description": "", 7 | "id": "addition;addition-of-two-numbers", 8 | "keyword": "Scenario", 9 | "line": 5, 10 | "name": "Addition of two numbers", 11 | "steps": [ 12 | { 13 | "arguments": [], 14 | "keyword": "Given ", 15 | "line": 6, 16 | "name": "I have number 6 in calculator", 17 | "match": { 18 | "location": "features\\support\\steps.js:5" 19 | }, 20 | "result": { 21 | "status": "passed", 22 | "duration": 1211400 23 | } 24 | }, 25 | { 26 | "arguments": [], 27 | "keyword": "When ", 28 | "line": 7, 29 | "name": "I entered number 7", 30 | "match": { 31 | "location": "features\\support\\steps.js:9" 32 | }, 33 | "result": { 34 | "status": "passed", 35 | "duration": 136500 36 | } 37 | }, 38 | { 39 | "arguments": [], 40 | "keyword": "Then ", 41 | "line": 8, 42 | "name": "I should see result 13", 43 | "match": { 44 | "location": "features\\support\\steps.js:13" 45 | }, 46 | "result": { 47 | "status": "passed", 48 | "duration": 244700 49 | } 50 | } 51 | ], 52 | "tags": [ 53 | { 54 | "name": "@green", 55 | "line": 4 56 | }, 57 | { 58 | "name": "@fast", 59 | "line": 4 60 | }, 61 | { 62 | "name": "@testCase=1234", 63 | "line": 4 64 | } 65 | ], 66 | "type": "scenario" 67 | } 68 | ], 69 | "id": "addition", 70 | "line": 1, 71 | "keyword": "Feature", 72 | "name": "Addition", 73 | "tags": [ 74 | { 75 | "name": "@blue", 76 | "line": 4 77 | }, 78 | { 79 | "name": "@slow", 80 | "line": 4 81 | }, 82 | { 83 | "name": "@suite=1234", 84 | "line": 4 85 | } 86 | ], 87 | "uri": "features\\sample.feature", 88 | "metadata": { 89 | "browser": { "name": "firefox", "version": "129.0" }, 90 | "device": "Desktop", 91 | "platform": { "name": "Windows", "version": "11" } 92 | } 93 | }, 94 | { 95 | "description": "Verify calculator functionalities", 96 | "elements": [ 97 | { 98 | "description": "", 99 | "id": "addition;addition-of-two-numbers", 100 | "keyword": "Scenario", 101 | "line": 5, 102 | "name": "Addition of two numbers", 103 | "steps": [ 104 | { 105 | "arguments": [], 106 | "keyword": "Given ", 107 | "line": 6, 108 | "name": "I have number 6 in calculator", 109 | "match": { 110 | "location": "features\\support\\steps.js:5" 111 | }, 112 | "result": { 113 | "status": "passed", 114 | "duration": 1211400 115 | } 116 | }, 117 | { 118 | "arguments": [], 119 | "keyword": "When ", 120 | "line": 7, 121 | "name": "I entered number 7", 122 | "match": { 123 | "location": "features\\support\\steps.js:9" 124 | }, 125 | "result": { 126 | "status": "passed", 127 | "duration": 136500 128 | } 129 | }, 130 | { 131 | "arguments": [], 132 | "keyword": "Then ", 133 | "line": 8, 134 | "name": "I should see result 13", 135 | "match": { 136 | "location": "features\\support\\steps.js:13" 137 | }, 138 | "result": { 139 | "status": "passed", 140 | "duration": 244700 141 | } 142 | } 143 | ], 144 | "tags": [ 145 | { 146 | "name": "@green", 147 | "line": 4 148 | }, 149 | { 150 | "name": "@fast", 151 | "line": 4 152 | }, 153 | { 154 | "name": "@testCase=1234", 155 | "line": 4 156 | } 157 | ], 158 | "type": "scenario" 159 | } 160 | ], 161 | "id": "addition", 162 | "line": 1, 163 | "keyword": "Feature", 164 | "name": "Addition", 165 | "tags": [ 166 | { 167 | "name": "@blue", 168 | "line": 4 169 | }, 170 | { 171 | "name": "@slow", 172 | "line": 4 173 | }, 174 | { 175 | "name": "@suite=1234", 176 | "line": 4 177 | } 178 | ], 179 | "uri": "features\\sample.feature", 180 | "metadata": { 181 | "browser": { "name": "chrome", "version": "129.0" }, 182 | "device": "Desktop", 183 | "platform": { "name": "Windows", "version": "11" } 184 | } 185 | } 186 | ] -------------------------------------------------------------------------------- /test/data/custom/custom-runner.js: -------------------------------------------------------------------------------- 1 | const p6 = require('pactum'); 2 | const assert = require('assert'); 3 | 4 | async function run({ target, extension, result }) { 5 | assert.equal(target.name, 'teams'); 6 | assert.equal(extension.name, 'custom'); 7 | assert.equal(result.name, 'Default suite'); 8 | await p6.spec().get('http://localhost:9393/custom'); 9 | } 10 | 11 | module.exports = { 12 | run 13 | } -------------------------------------------------------------------------------- /test/data/custom/custom-target-runner.js: -------------------------------------------------------------------------------- 1 | const p6 = require('pactum'); 2 | const assert = require('assert'); 3 | 4 | async function run({ target, result }) { 5 | assert.equal(target.name, 'custom'); 6 | assert.equal(result.name, 'Default suite'); 7 | await p6.spec().get('http://localhost:9393/custom'); 8 | } 9 | 10 | module.exports = { 11 | run 12 | } -------------------------------------------------------------------------------- /test/data/jmeter/sample.csv: -------------------------------------------------------------------------------- 1 | Label,# Samples,Average,Median,90% Line,95% Line,99% Line,Min,Max,Error %,Throughput,Received KB/sec,Sent KB/sec 2 | S01_T01_Application_Launch,10,3086,2832,3795,3795,3797,2119,3797,0.001%,.14422,2662.79,5.36 3 | S01_T02_Application_Login,9,4355,3273,4416,10786,10786,3042,10786,0.000%,.14610,2754.90,12.94 4 | TOTAL,39,4660,3318,11354,11446,15513,1135,15513,0.000%,.55535,5166.44,38.87 5 | -------------------------------------------------------------------------------- /test/data/junit/single-suite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Some Text 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/data/playwright/example-get-started-link-chromium/test-failed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/test/data/playwright/example-get-started-link-chromium/test-failed-1.png -------------------------------------------------------------------------------- /test/data/playwright/example-get-started-link-firefox/test-failed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/test/data/playwright/example-get-started-link-firefox/test-failed-1.png -------------------------------------------------------------------------------- /test/data/playwright/junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | Call log: 14 | - expect.toBeVisible with timeout 5000ms 15 | - waiting for getByRole('heading', { name: 'Installations' }) 16 | 17 | 18 | 15 | 19 | 16 | // Expects page to have a heading with the name of Installation. 20 | > 17 | await expect(page.getByRole('heading', { name: 'Installations' })).toBeVisible(); 21 | | ^ 22 | 18 | }); 23 | 19 | 24 | 25 | at /Users/anudeep/Documents/my/repos/test-results-reporter/example-playwright-testbeats/tests/example.spec.ts:17:70 26 | 27 | attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── 28 | test-results/example-get-started-link-chromium/test-failed-1.png 29 | ──────────────────────────────────────────────────────────────────────────────────────────────── 30 | ]]> 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | Call log: 51 | - expect.toBeVisible with timeout 5000ms 52 | - waiting for getByRole('heading', { name: 'Installations' }) 53 | 54 | 55 | 15 | 56 | 16 | // Expects page to have a heading with the name of Installation. 57 | > 17 | await expect(page.getByRole('heading', { name: 'Installations' })).toBeVisible(); 58 | | ^ 59 | 18 | }); 60 | 19 | 61 | 62 | at /Users/anudeep/Documents/my/repos/test-results-reporter/example-playwright-testbeats/tests/example.spec.ts:17:70 63 | 64 | attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── 65 | test-results/example-get-started-link-firefox/test-failed-1.png 66 | ──────────────────────────────────────────────────────────────────────────────────────────────── 67 | ]]> 68 | 69 | 70 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /test/data/testng/single-suite-failures.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/data/testng/single-suite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/ext-global.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('global extensions', () => { 6 | 7 | afterEach(() => { 8 | mock.clearInteractions(); 9 | }); 10 | 11 | it('with global extensions', async () => { 12 | const id = mock.addInteraction('post test-summary with metadata to teams'); 13 | await publish({ 14 | config: { 15 | "targets": [ 16 | { 17 | "name": "teams", 18 | "inputs": { 19 | "url": "http://localhost:9393/message" 20 | }, 21 | } 22 | ], 23 | "extensions": [ 24 | { 25 | "name": "metadata", 26 | "inputs": { 27 | "data": [ 28 | { 29 | "key": "Browser", 30 | "value": "Chrome" 31 | }, 32 | { 33 | "value": "1920*1080" 34 | }, 35 | { 36 | "value": "1920*1080", 37 | "condition": "never" 38 | }, 39 | { 40 | "key": "Pipeline", 41 | "value": "some-url", 42 | "type": "hyperlink" 43 | }, 44 | ] 45 | } 46 | } 47 | ], 48 | "results": [ 49 | { 50 | "type": "testng", 51 | "files": [ 52 | "test/data/testng/single-suite.xml" 53 | ] 54 | } 55 | ] 56 | } 57 | }); 58 | assert.equal(mock.getInteraction(id).exercised, true); 59 | }); 60 | 61 | it('with global and normal extensions', async () => { 62 | const id = mock.addInteraction('post test-summary with metadata and hyperlinks to teams'); 63 | await publish({ 64 | config: { 65 | "targets": [ 66 | { 67 | "name": "teams", 68 | "inputs": { 69 | "url": "http://localhost:9393/message" 70 | }, 71 | "extensions": [ 72 | { 73 | "name": "hyperlinks", 74 | "inputs": { 75 | "links": [ 76 | { 77 | "text": "Pipeline", 78 | "url": "some-url" 79 | }, 80 | { 81 | "text": "Video", 82 | "url": "some-url", 83 | "condition": "pass" 84 | } 85 | ] 86 | } 87 | } 88 | ], 89 | } 90 | ], 91 | "extensions": [ 92 | { 93 | "name": "metadata", 94 | "inputs": { 95 | "data": [ 96 | { 97 | "key": "Browser", 98 | "value": "Chrome" 99 | }, 100 | { 101 | "value": "1920*1080" 102 | }, 103 | { 104 | "value": "1920*1080", 105 | "condition": "never" 106 | }, 107 | { 108 | "key": "Pipeline", 109 | "value": "some-url", 110 | "type": "hyperlink" 111 | }, 112 | ] 113 | } 114 | } 115 | ], 116 | "results": [ 117 | { 118 | "type": "testng", 119 | "files": [ 120 | "test/data/testng/single-suite.xml" 121 | ] 122 | } 123 | ] 124 | } 125 | }); 126 | assert.equal(mock.getInteraction(id).exercised, true); 127 | }); 128 | 129 | }); -------------------------------------------------------------------------------- /test/ext.browserstack.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('extensions - browserstack', () => { 6 | 7 | it('should send test-summary with browserstack to teams', async () => { 8 | const id1 = mock.addInteraction('get automation builds'); 9 | const id2 = mock.addInteraction('get automation build sessions'); 10 | const id3 = mock.addInteraction('post test-summary with browserstack to teams'); 11 | await publish({ 12 | config: { 13 | "targets": [ 14 | { 15 | "name": "teams", 16 | "inputs": { 17 | "url": "http://localhost:9393/message" 18 | }, 19 | } 20 | ], 21 | "extensions": [ 22 | { 23 | "name": "browserstack", 24 | "inputs": { 25 | "url": "http://localhost:9393", 26 | "username": "username", 27 | "access_key": "access_key", 28 | "automation_build_name": "build-name" 29 | } 30 | } 31 | ], 32 | "results": [ 33 | { 34 | "type": "testng", 35 | "files": [ 36 | "test/data/testng/single-suite.xml" 37 | ] 38 | } 39 | ] 40 | } 41 | }); 42 | assert.equal(mock.getInteraction(id1).exercised, true); 43 | assert.equal(mock.getInteraction(id2).exercised, true); 44 | assert.equal(mock.getInteraction(id3).exercised, true); 45 | }); 46 | 47 | it('should send test-summary with browserstack to teams without automation build name', async () => { 48 | const id1 = mock.addInteraction('get automation builds'); 49 | const id2 = mock.addInteraction('post test-summary to teams'); 50 | await publish({ 51 | config: { 52 | "targets": [ 53 | { 54 | "name": "teams", 55 | "inputs": { 56 | "url": "http://localhost:9393/message" 57 | }, 58 | } 59 | ], 60 | "extensions": [ 61 | { 62 | "name": "browserstack", 63 | "inputs": { 64 | "url": "http://localhost:9393", 65 | "username": "username", 66 | "access_key": "access_key", 67 | "automation_build_name": "invalid-build-name" 68 | } 69 | } 70 | ], 71 | "results": [ 72 | { 73 | "type": "testng", 74 | "files": [ 75 | "test/data/testng/single-suite.xml" 76 | ] 77 | } 78 | ] 79 | } 80 | }); 81 | assert.equal(mock.getInteraction(id1).exercised, true); 82 | assert.equal(mock.getInteraction(id2).exercised, true); 83 | }); 84 | 85 | afterEach(() => { 86 | mock.clearInteractions(); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /test/ext.custom.spec.js: -------------------------------------------------------------------------------- 1 | const { mock, spec } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('extensions - custom', () => { 6 | 7 | it('load from fs', async () => { 8 | const id1 = mock.addInteraction('post test-summary to teams'); 9 | const id2 = mock.addInteraction('get custom'); 10 | await publish({ 11 | config: { 12 | "targets": [ 13 | { 14 | "name": "teams", 15 | "inputs": { 16 | "url": "http://localhost:9393/message" 17 | }, 18 | "extensions": [ 19 | { 20 | "name": "custom", 21 | "inputs": { 22 | "load": "test/data/custom/custom-runner.js" 23 | } 24 | } 25 | ] 26 | } 27 | ], 28 | "results": [ 29 | { 30 | "type": "testng", 31 | "files": [ 32 | "test/data/testng/single-suite.xml" 33 | ] 34 | } 35 | ] 36 | } 37 | }); 38 | assert.equal(mock.getInteraction(id1).exercised, true); 39 | assert.equal(mock.getInteraction(id2).exercised, true); 40 | }); 41 | 42 | it('load from inline function', async () => { 43 | const id1 = mock.addInteraction('post test-summary to teams'); 44 | const id2 = mock.addInteraction('get custom'); 45 | await publish({ 46 | config: { 47 | "targets": [ 48 | { 49 | "name": "teams", 50 | "inputs": { 51 | "url": "http://localhost:9393/message" 52 | }, 53 | "extensions": [ 54 | { 55 | "name": "custom", 56 | "inputs": { 57 | "load": async function ({ target, extension, result }) { 58 | assert.equal(target.name, 'teams'); 59 | assert.equal(extension.name, 'custom'); 60 | assert.equal(result.name, 'Default suite'); 61 | await spec().get('http://localhost:9393/custom'); 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | ], 68 | "results": [ 69 | { 70 | "type": "junit", 71 | "files": [ 72 | "test/data/junit/single-suite.xml" 73 | ] 74 | } 75 | ] 76 | } 77 | }); 78 | assert.equal(mock.getInteraction(id1).exercised, true); 79 | assert.equal(mock.getInteraction(id2).exercised, true); 80 | }); 81 | 82 | it('invalid load', async () => { 83 | const id1 = mock.addInteraction('post test-summary to teams'); 84 | await publish({ 85 | config: { 86 | "targets": [ 87 | { 88 | "name": "teams", 89 | "inputs": { 90 | "url": "http://localhost:9393/message" 91 | }, 92 | "extensions": [ 93 | { 94 | "name": "custom", 95 | "inputs": { 96 | "load": {} 97 | } 98 | } 99 | ] 100 | } 101 | ], 102 | "results": [ 103 | { 104 | "type": "junit", 105 | "files": [ 106 | "test/data/junit/single-suite.xml" 107 | ] 108 | } 109 | ] 110 | } 111 | }); 112 | assert.equal(mock.getInteraction(id1).exercised, true); 113 | }); 114 | 115 | afterEach(() => { 116 | mock.clearInteractions(); 117 | }); 118 | 119 | }); -------------------------------------------------------------------------------- /test/ext.metadata.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish, defineConfig } = require('../src'); 4 | 5 | describe('extensions - metadata', () => { 6 | 7 | it('should send test-summary with metadata to teams', async () => { 8 | const id = mock.addInteraction('post test-summary with metadata to teams'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "teams", 14 | "inputs": { 15 | "url": "http://localhost:9393/message" 16 | }, 17 | "extensions": [ 18 | { 19 | "name": "metadata", 20 | "inputs": { 21 | "data": [ 22 | { 23 | "key": "Browser", 24 | "value": "Chrome" 25 | }, 26 | { 27 | "value": "1920*1080" 28 | }, 29 | { 30 | "value": "1920*1080", 31 | "condition": "never" 32 | }, 33 | { 34 | "key": "Pipeline", 35 | "value": "some-url", 36 | "type": "hyperlink" 37 | }, 38 | ] 39 | } 40 | } 41 | ] 42 | } 43 | ], 44 | "results": [ 45 | { 46 | "type": "testng", 47 | "files": [ 48 | "test/data/testng/single-suite.xml" 49 | ] 50 | } 51 | ] 52 | } 53 | }); 54 | assert.equal(mock.getInteraction(id).exercised, true); 55 | }); 56 | 57 | it('should send test-summary with metadata to slack', async () => { 58 | const id = mock.addInteraction('post test-summary with metadata to slack'); 59 | await publish({ 60 | config: { 61 | "targets": [ 62 | { 63 | "name": "slack", 64 | "inputs": { 65 | "url": "http://localhost:9393/message" 66 | }, 67 | "extensions": [ 68 | { 69 | "name": "metadata", 70 | "inputs": { 71 | "data": [ 72 | { 73 | "key": "Browser", 74 | "value": "Chrome" 75 | }, 76 | { 77 | "value": "1920*1080" 78 | }, 79 | { 80 | "value": "1920*1080", 81 | "condition": "never" 82 | }, 83 | { 84 | "key": "Pipeline", 85 | "value": "some-url", 86 | "type": "hyperlink" 87 | }, 88 | ] 89 | } 90 | } 91 | ] 92 | } 93 | ], 94 | "results": [ 95 | { 96 | "type": "testng", 97 | "files": [ 98 | "test/data/testng/single-suite.xml" 99 | ] 100 | } 101 | ] 102 | } 103 | }); 104 | assert.equal(mock.getInteraction(id).exercised, true); 105 | }); 106 | 107 | it('should send test-summary with metadata to chat', async () => { 108 | const id = mock.addInteraction('post test-summary with metadata to chat'); 109 | await publish({ 110 | config: { 111 | "targets": [ 112 | { 113 | "name": "chat", 114 | "inputs": { 115 | "url": "http://localhost:9393/message" 116 | }, 117 | "extensions": [ 118 | { 119 | "name": "metadata", 120 | "inputs": { 121 | "data": [ 122 | { 123 | "key": "Browser", 124 | "value": "Chrome" 125 | }, 126 | { 127 | "value": "1920*1080" 128 | }, 129 | { 130 | "value": "1920*1080", 131 | "condition": "never" 132 | }, 133 | { 134 | "key": "Pipeline", 135 | "value": "some-url", 136 | "type": "hyperlink" 137 | }, 138 | ] 139 | } 140 | } 141 | ] 142 | } 143 | ], 144 | "results": [ 145 | { 146 | "type": "testng", 147 | "files": [ 148 | "test/data/testng/single-suite.xml" 149 | ] 150 | } 151 | ] 152 | } 153 | }); 154 | assert.equal(mock.getInteraction(id).exercised, true); 155 | }); 156 | 157 | afterEach(() => { 158 | mock.clearInteractions(); 159 | }); 160 | 161 | }); -------------------------------------------------------------------------------- /test/ext.quick-chart-test-summary.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('extensions - quick-chart-test-summary', () => { 6 | 7 | it('should send test-summary with links to teams', async () => { 8 | const id = mock.addInteraction('post test-summary to teams with qc-test-summary', { quickChartUrl: "https://quickchart.io" }); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "teams", 14 | "inputs": { 15 | "url": "http://localhost:9393/message" 16 | }, 17 | "extensions": [ 18 | { 19 | "name": "quick-chart-test-summary" 20 | } 21 | ] 22 | } 23 | ], 24 | "results": [ 25 | { 26 | "type": "testng", 27 | "files": [ 28 | "test/data/testng/single-suite-failures.xml" 29 | ] 30 | } 31 | ] 32 | } 33 | }); 34 | assert.equal(mock.getInteraction(id).exercised, true); 35 | }); 36 | 37 | it('should send test-summary with links to slack', async () => { 38 | const id = mock.addInteraction('post test-summary to slack with qc-test-summary', { quickChartUrl: "https://quickchart.io" }); 39 | await publish({ 40 | config: { 41 | "targets": [ 42 | { 43 | "name": "slack", 44 | "inputs": { 45 | "url": "http://localhost:9393/message" 46 | }, 47 | "extensions": [ 48 | { 49 | "name": "quick-chart-test-summary" 50 | } 51 | ] 52 | } 53 | ], 54 | "results": [ 55 | { 56 | "type": "testng", 57 | "files": [ 58 | "test/data/testng/single-suite-failures.xml" 59 | ] 60 | } 61 | ] 62 | } 63 | }); 64 | assert.equal(mock.getInteraction(id).exercised, true); 65 | }); 66 | 67 | it('should send test-summary with links to teams with custom qc-test server', async () => { 68 | const id = mock.addInteraction('post test-summary to teams with qc-test-summary', { quickChartUrl: "https://demo.quickchart.example.com" }); 69 | await publish({ 70 | config: { 71 | "targets": [ 72 | { 73 | "name": "teams", 74 | "inputs": { 75 | "url": "http://localhost:9393/message" 76 | }, 77 | "extensions": [ 78 | { 79 | "name": "quick-chart-test-summary", 80 | "inputs": { 81 | "url": "https://demo.quickchart.example.com" 82 | } 83 | } 84 | ] 85 | } 86 | ], 87 | "results": [ 88 | { 89 | "type": "testng", 90 | "files": [ 91 | "test/data/testng/single-suite-failures.xml" 92 | ] 93 | } 94 | ] 95 | } 96 | }); 97 | assert.equal(mock.getInteraction(id).exercised, true); 98 | }); 99 | 100 | it('should send test-summary with links to slack with custom qc-test server', async () => { 101 | const id = mock.addInteraction('post test-summary to slack with qc-test-summary', { quickChartUrl: "https://demo.quickchart.example.com" }); 102 | await publish({ 103 | config: { 104 | "targets": [ 105 | { 106 | "name": "slack", 107 | "inputs": { 108 | "url": "http://localhost:9393/message" 109 | }, 110 | "extensions": [ 111 | { 112 | "name": "quick-chart-test-summary", 113 | "inputs": { 114 | "url": "https://demo.quickchart.example.com" 115 | } 116 | } 117 | ] 118 | } 119 | ], 120 | "results": [ 121 | { 122 | "type": "testng", 123 | "files": [ 124 | "test/data/testng/single-suite-failures.xml" 125 | ] 126 | } 127 | ] 128 | } 129 | }); 130 | assert.equal(mock.getInteraction(id).exercised, true); 131 | }); 132 | 133 | it('should send test-summary with links to teams and default qc-test server if url empty', async () => { 134 | const id = mock.addInteraction('post test-summary to teams with qc-test-summary', { quickChartUrl: "https://quickchart.io" }); 135 | await publish({ 136 | config: { 137 | "targets": [ 138 | { 139 | "name": "teams", 140 | "inputs": { 141 | "url": "http://localhost:9393/message" 142 | }, 143 | "extensions": [ 144 | { 145 | "name": "quick-chart-test-summary", 146 | "inputs": { 147 | "url": " " 148 | } 149 | } 150 | ] 151 | } 152 | ], 153 | "results": [ 154 | { 155 | "type": "testng", 156 | "files": [ 157 | "test/data/testng/single-suite-failures.xml" 158 | ] 159 | } 160 | ] 161 | } 162 | }); 163 | assert.equal(mock.getInteraction(id).exercised, true); 164 | }); 165 | 166 | 167 | afterEach(() => { 168 | mock.clearInteractions(); 169 | }); 170 | 171 | }); -------------------------------------------------------------------------------- /test/handle-errors.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('handle errors', () => { 6 | 7 | afterEach(() => { 8 | mock.clearInteractions(); 9 | }); 10 | 11 | it('should send errors to chat', async () => { 12 | const id = mock.addInteraction('post errors to chat'); 13 | let err; 14 | try { 15 | await publish({ 16 | config: { 17 | "targets": [ 18 | { 19 | "name": "chat", 20 | "inputs": { 21 | "url": "http://localhost:9393/message" 22 | } 23 | } 24 | ], 25 | "results": [ 26 | { 27 | "type": "testng", 28 | "files": [ 29 | "test/data/testng/invalid.xml" 30 | ] 31 | } 32 | ] 33 | } 34 | }); 35 | } catch (e) { 36 | err = e; 37 | } 38 | assert.equal(mock.getInteraction(id).exercised, true); 39 | assert.ok(err.toString().includes('invalid.xml')); 40 | }); 41 | 42 | it('should send errors to teams', async () => { 43 | const id = mock.addInteraction('post errors to teams'); 44 | let err; 45 | try { 46 | await publish({ 47 | config: { 48 | "targets": [ 49 | { 50 | "name": "teams", 51 | "inputs": { 52 | "url": "http://localhost:9393/message" 53 | } 54 | } 55 | ], 56 | "results": [ 57 | { 58 | "type": "testng", 59 | "files": [ 60 | "test/data/testng/invalid.xml" 61 | ] 62 | } 63 | ] 64 | } 65 | }); 66 | } catch (e) { 67 | err = e; 68 | } 69 | assert.equal(mock.getInteraction(id).exercised, true); 70 | assert.ok(err.toString().includes('invalid.xml')); 71 | }); 72 | 73 | it('should send errors to slack', async () => { 74 | const id = mock.addInteraction('post errors to slack'); 75 | let err; 76 | try { 77 | await publish({ 78 | config: { 79 | "targets": [ 80 | { 81 | "name": "slack", 82 | "inputs": { 83 | "url": "http://localhost:9393/message" 84 | } 85 | } 86 | ], 87 | "results": [ 88 | { 89 | "type": "testng", 90 | "files": [ 91 | "test/data/testng/invalid.xml" 92 | ] 93 | } 94 | ] 95 | } 96 | }); 97 | } catch (e) { 98 | err = e; 99 | } 100 | assert.equal(mock.getInteraction(id).exercised, true); 101 | assert.ok(err.toString().includes('invalid.xml')); 102 | }); 103 | 104 | it('should send results and errors to chat', async () => { 105 | const id1 = mock.addInteraction('post test-summary to chat'); 106 | const id2 = mock.addInteraction('post errors to chat'); 107 | let err; 108 | try { 109 | await publish({ 110 | config: { 111 | "targets": [ 112 | { 113 | "name": "chat", 114 | "inputs": { 115 | "url": "http://localhost:9393/message" 116 | } 117 | } 118 | ], 119 | "results": [ 120 | { 121 | "type": "testng", 122 | "files": [ 123 | "test/data/testng/single-suite.xml", 124 | "test/data/testng/invalid.xml" 125 | ] 126 | } 127 | ] 128 | } 129 | }); 130 | } catch (e) { 131 | err = e; 132 | } 133 | assert.equal(mock.getInteraction(id1).exercised, true); 134 | assert.equal(mock.getInteraction(id2).exercised, true); 135 | assert.ok(err.toString().includes('invalid.xml')); 136 | }); 137 | 138 | it('should send results and errors to teams', async () => { 139 | const id1 = mock.addInteraction('post test-summary to teams'); 140 | const id2 = mock.addInteraction('post errors to teams'); 141 | let err; 142 | try { 143 | await publish({ 144 | config: { 145 | "targets": [ 146 | { 147 | "name": "teams", 148 | "inputs": { 149 | "url": "http://localhost:9393/message" 150 | } 151 | } 152 | ], 153 | "results": [ 154 | { 155 | "type": "testng", 156 | "files": [ 157 | "test/data/testng/single-suite.xml", 158 | "test/data/testng/invalid.xml" 159 | ] 160 | } 161 | ] 162 | } 163 | }); 164 | } catch (e) { 165 | err = e; 166 | } 167 | assert.equal(mock.getInteraction(id1).exercised, true); 168 | assert.equal(mock.getInteraction(id2).exercised, true); 169 | assert.ok(err.toString().includes('invalid.xml')); 170 | }); 171 | 172 | it('should send results and errors to slack', async () => { 173 | const id1 = mock.addInteraction('post test-summary to slack'); 174 | const id2 = mock.addInteraction('post errors to slack'); 175 | let err; 176 | try { 177 | await publish({ 178 | config: { 179 | "targets": [ 180 | { 181 | "name": "slack", 182 | "inputs": { 183 | "url": "http://localhost:9393/message" 184 | } 185 | } 186 | ], 187 | "results": [ 188 | { 189 | "type": "testng", 190 | "files": [ 191 | "test/data/testng/single-suite.xml", 192 | "test/data/testng/invalid.xml" 193 | ] 194 | } 195 | ] 196 | } 197 | }); 198 | } catch (e) { 199 | err = e; 200 | } 201 | assert.equal(mock.getInteraction(id1).exercised, true); 202 | assert.equal(mock.getInteraction(id2).exercised, true); 203 | assert.ok(err.toString().includes('invalid.xml')); 204 | }); 205 | 206 | }); -------------------------------------------------------------------------------- /test/mocks/beats.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('post test results to beats', () => { 4 | return { 5 | strict: false, 6 | request: { 7 | method: 'POST', 8 | path: '/api/core/v1/test-runs' 9 | }, 10 | response: { 11 | status: 200, 12 | body: { 13 | id: 'test-run-id' 14 | } 15 | } 16 | } 17 | }); 18 | 19 | addInteractionHandler('get test results from beats', () => { 20 | return { 21 | strict: false, 22 | request: { 23 | method: 'GET', 24 | path: '/api/core/v1/test-runs/test-run-id', 25 | }, 26 | response: { 27 | status: 200, 28 | body: { 29 | id: 'test-run-id', 30 | "failure_summary_status": "COMPLETED", 31 | "failure_analysis_status": "COMPLETED", 32 | "smart_analysis_status": "COMPLETED", 33 | "execution_metrics": [ 34 | { 35 | "failure_summary": "test failure summary" 36 | } 37 | ] 38 | } 39 | } 40 | } 41 | }); 42 | 43 | addInteractionHandler('get test results with smart analysis from beats', () => { 44 | return { 45 | strict: false, 46 | request: { 47 | method: 'GET', 48 | path: '/api/core/v1/test-runs/test-run-id' 49 | }, 50 | response: { 51 | status: 200, 52 | body: { 53 | id: 'test-run-id', 54 | "failure_summary_status": "COMPLETED", 55 | "smart_analysis_status": "SKIPPED", 56 | "execution_metrics": [ 57 | { 58 | "failure_summary": "", 59 | "newly_failed": 1, 60 | "always_failing": 1, 61 | "recovered": 1, 62 | "added": 0, 63 | "removed": 0, 64 | "flaky": 1, 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | }); 71 | 72 | addInteractionHandler('get test results with failure analysis from beats', () => { 73 | return { 74 | strict: false, 75 | request: { 76 | method: 'GET', 77 | path: '/api/core/v1/test-runs/test-run-id' 78 | }, 79 | response: { 80 | status: 200, 81 | body: { 82 | id: 'test-run-id', 83 | "failure_summary_status": "COMPLETED", 84 | "failure_analysis_status": "COMPLETED", 85 | "smart_analysis_status": "SKIPPED", 86 | "execution_metrics": [ 87 | { 88 | "failure_summary": "", 89 | "newly_failed": 1, 90 | "always_failing": 1, 91 | "recovered": 1, 92 | "added": 0, 93 | "removed": 0, 94 | "flaky": 1, 95 | "product_bugs": 1, 96 | "environment_issues": 1, 97 | "automation_bugs": 1, 98 | "not_a_defects": 1, 99 | "to_investigate": 1, 100 | "auto_analysed": 1 101 | } 102 | ] 103 | } 104 | } 105 | } 106 | }); 107 | 108 | addInteractionHandler('get error clusters from beats', () => { 109 | return { 110 | strict: false, 111 | request: { 112 | method: 'GET', 113 | path: '/api/core/v1/test-runs/test-run-id/error-clusters', 114 | queryParams: { 115 | "limit": 3 116 | } 117 | }, 118 | response: { 119 | status: 200, 120 | body: { 121 | values: [ 122 | { 123 | test_failure_id: 'test-failure-id', 124 | failure: 'failure two', 125 | count: 2 126 | }, 127 | { 128 | test_failure_id: 'test-failure-id', 129 | failure: 'failure one', 130 | count: 1 131 | } 132 | ] 133 | } 134 | } 135 | } 136 | }); 137 | 138 | addInteractionHandler('get empty error clusters from beats', () => { 139 | return { 140 | strict: false, 141 | request: { 142 | method: 'GET', 143 | path: '/api/core/v1/test-runs/test-run-id/error-clusters', 144 | queryParams: { 145 | "limit": 3 146 | } 147 | }, 148 | response: { 149 | status: 200, 150 | body: { 151 | values: [] 152 | } 153 | } 154 | } 155 | }); 156 | 157 | addInteractionHandler('get failure analysis from beats', () => { 158 | return { 159 | strict: false, 160 | request: { 161 | method: 'GET', 162 | path: '/api/core/v1/test-runs/test-run-id/failure-analysis' 163 | }, 164 | response: { 165 | status: 200, 166 | body: [ 167 | { 168 | name: 'To Investigate', 169 | count: 1 170 | }, 171 | { 172 | name: 'Auto Analysed', 173 | count: 1 174 | } 175 | ] 176 | } 177 | } 178 | }); 179 | 180 | addInteractionHandler('upload attachments', () => { 181 | return { 182 | strict: false, 183 | request: { 184 | method: 'POST', 185 | path: '/api/core/v1/test-cases/attachments', 186 | }, 187 | response: { 188 | status: 200, 189 | } 190 | } 191 | }) -------------------------------------------------------------------------------- /test/mocks/browserstack.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('get automation builds', () => { 4 | return { 5 | strict: false, 6 | request: { 7 | method: 'GET', 8 | path: '/automate/builds.json', 9 | }, 10 | response: { 11 | status: 200, 12 | body: [ 13 | { 14 | "automation_build": { 15 | "name": "build-name", 16 | "hashed_id": "build-hashed-id", 17 | "duration": 176, 18 | "status": "done", 19 | "build_tag": "full", 20 | "public_url": "https://automate.browserstack.com/dashboard/v2/public-build/build-public-url" 21 | } 22 | } 23 | ] 24 | } 25 | } 26 | }); 27 | 28 | addInteractionHandler('get automation build sessions', () => { 29 | return { 30 | strict: false, 31 | request: { 32 | method: 'GET', 33 | path: '/automate/builds/build-hashed-id/sessions.json', 34 | }, 35 | response: { 36 | status: 200, 37 | body: [ 38 | { 39 | "automation_session": { 40 | "name": "session-name", 41 | "duration": 176, 42 | "os": "Windows", 43 | "os_version": "10", 44 | "browser_version": "10", 45 | "browser": "Chrome", 46 | "device": "iPhone 12 Pro", 47 | "status": "done", 48 | "hashed_id": "session-hashed-id", 49 | "public_url": "https://automate.browserstack.com/dashboard/v2/public-build/build-public-url" 50 | } 51 | } 52 | ] 53 | } 54 | } 55 | }); -------------------------------------------------------------------------------- /test/mocks/custom.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('get custom', () => { 4 | return { 5 | request: { 6 | method: 'GET', 7 | path: '/custom' 8 | }, 9 | response: { 10 | status: 200 11 | } 12 | } 13 | }); -------------------------------------------------------------------------------- /test/mocks/index.js: -------------------------------------------------------------------------------- 1 | require('./beats.mock'); 2 | require('./browserstack.mock'); 3 | require('./custom.mock'); 4 | require('./rp.mock'); 5 | require('./slack.mock'); 6 | require('./teams.mock'); 7 | require('./chat.mock'); 8 | require('./percy.mock'); 9 | require('./influx.mock'); 10 | require('./influx2.mock'); -------------------------------------------------------------------------------- /test/mocks/influx.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('save perf results', () => { 4 | return { 5 | request: { 6 | method: 'POST', 7 | path: '/write', 8 | headers: { 9 | "authorization": "Basic dXNlcjpwYXNz" 10 | }, 11 | queryParams: { 12 | "db": "TestResults" 13 | }, 14 | body: "PerfRun,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=2729683,data_sent_rate=38.87,data_received_sum=362818330,data_received_rate=5166.44\nPerfTransaction,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=371654,data_sent_rate=5.36,data_received_sum=184633892,data_received_rate=2662.79\nPerfTransaction,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=797125,data_sent_rate=12.94,data_received_sum=169706365,data_received_rate=2754.9" 15 | }, 16 | response: { 17 | status: 200 18 | } 19 | } 20 | }); 21 | 22 | addInteractionHandler('save perf results with custom tags and fields', () => { 23 | return { 24 | request: { 25 | method: 'POST', 26 | path: '/write', 27 | headers: { 28 | "authorization": "Basic dXNlcjpwYXNz" 29 | }, 30 | queryParams: { 31 | "db": "TestResults" 32 | }, 33 | body: "PerfRun,Team=QA,App=PactumJS,Name=TOTAL,Status=PASS id=123,status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=2729683,data_sent_rate=38.87,data_received_sum=362818330,data_received_rate=5166.44\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T01_Application_Launch,Status=PASS id=123,status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=371654,data_sent_rate=5.36,data_received_sum=184633892,data_received_rate=2662.79\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T02_Application_Login,Status=PASS id=123,status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=797125,data_sent_rate=12.94,data_received_sum=169706365,data_received_rate=2754.9" 34 | }, 35 | response: { 36 | status: 200 37 | } 38 | } 39 | }); 40 | 41 | addInteractionHandler('save test results', () => { 42 | return { 43 | request: { 44 | method: 'POST', 45 | path: '/write', 46 | headers: { 47 | "authorization": "Basic dXNlcjpwYXNz" 48 | }, 49 | queryParams: { 50 | "db": "TestResults" 51 | }, 52 | body: "TestRun,Name=Default\\ suite,Status=PASS status=0,total=4,passed=4,failed=0,duration=2000\nTestSuite,Name=Default\\ test,Status=PASS status=0,total=4,passed=4,failed=0,duration=2000\nTestCase,Name=c2,Status=PASS status=0,duration=0\nTestCase,Name=c3,Status=PASS status=0,duration=10\nTestCase,Name=c1,Status=PASS status=0,duration=0\nTestCase,Name=c4,Status=PASS status=0,duration=0" 53 | }, 54 | response: { 55 | status: 200 56 | } 57 | } 58 | }); 59 | 60 | addInteractionHandler('save test results with custom tags and fields', () => { 61 | return { 62 | request: { 63 | method: 'POST', 64 | path: '/write', 65 | headers: { 66 | "authorization": "Basic dXNlcjpwYXNz" 67 | }, 68 | queryParams: { 69 | "db": "TestResults" 70 | }, 71 | body: "TestRun,Team=QA,App=PactumJS,Name=Staging\\ -\\ UI\\ Smoke\\ Test\\ Run,Status=FAIL id=123,status=1,total=2,passed=1,failed=1,duration=1883597\nTestSuite,Team=QA,App=PactumJS,Name=desktop-chrome,Status=PASS id=123,status=0,total=1,passed=1,failed=0,duration=1164451\nTestCase,Team=QA,App=PactumJS,Name=GU,Status=PASS id=123,status=0,duration=243789\nTestSuite,Team=QA,App=PactumJS,Name=mobile-andoid,Status=FAIL id=123,status=1,total=1,passed=0,failed=1,duration=714100\nTestCase,Team=QA,App=PactumJS,Name=GU,Status=FAIL id=123,status=1,duration=156900" 72 | }, 73 | response: { 74 | status: 200 75 | } 76 | } 77 | }); -------------------------------------------------------------------------------- /test/mocks/influx2.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('save perf results to influx2', () => { 4 | return { 5 | request: { 6 | method: 'POST', 7 | path: '/api/v2/write', 8 | headers: { 9 | "authorization": "Token testtoken" 10 | }, 11 | queryParams: { 12 | "org": "testorg", 13 | "bucket": "testbucket", 14 | "precision": "ns" 15 | }, 16 | body: "PerfRun,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=2729683,data_sent_rate=38.87,data_received_sum=362818330,data_received_rate=5166.44\n"+ 17 | "PerfTransaction,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=371654,data_sent_rate=5.36,data_received_sum=184633892,data_received_rate=2662.79\n"+ 18 | "PerfTransaction,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=797125,data_sent_rate=12.94,data_received_sum=169706365,data_received_rate=2754.9" 19 | }, 20 | response: { 21 | status: 204 22 | } 23 | } 24 | }); 25 | 26 | addInteractionHandler('save perf results with custom tags and fields to influx2', () => { 27 | return { 28 | request: { 29 | method: 'POST', 30 | path: '/api/v2/write', 31 | headers: { 32 | "authorization": "Token testtoken" 33 | }, 34 | queryParams: { 35 | "org": "testorg", 36 | "bucket": "testbucket", 37 | "precision": "ns" 38 | }, 39 | body: "PerfRun,Team=QA,App=PactumJS,Name=TOTAL,Status=PASS id=123,status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=2729683,data_sent_rate=38.87,data_received_sum=362818330,data_received_rate=5166.44\n"+ 40 | "PerfTransaction,Team=QA,App=PactumJS,Name=S01_T01_Application_Launch,Status=PASS id=123,status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=371654,data_sent_rate=5.36,data_received_sum=184633892,data_received_rate=2662.79\n"+ 41 | "PerfTransaction,Team=QA,App=PactumJS,Name=S01_T02_Application_Login,Status=PASS id=123,status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=797125,data_sent_rate=12.94,data_received_sum=169706365,data_received_rate=2754.9" 42 | }, 43 | response: { 44 | status: 204 45 | } 46 | } 47 | }); 48 | 49 | addInteractionHandler('save test results to influx2', () => { 50 | return { 51 | request: { 52 | method: 'POST', 53 | path: '/api/v2/write', 54 | headers: { 55 | "authorization": "Token testtoken" 56 | }, 57 | queryParams: { 58 | "org": "testorg", 59 | "bucket": "testbucket", 60 | "precision": "ns" 61 | }, 62 | body: "TestRun,Name=Default\\ suite,Status=PASS status=0,total=4,passed=4,failed=0,duration=2000\n"+ 63 | "TestSuite,Name=Default\\ test,Status=PASS status=0,total=4,passed=4,failed=0,duration=2000\n"+ 64 | "TestCase,Name=c2,Status=PASS status=0,duration=0\n"+ 65 | "TestCase,Name=c3,Status=PASS status=0,duration=10\n"+ 66 | "TestCase,Name=c1,Status=PASS status=0,duration=0\n"+ 67 | "TestCase,Name=c4,Status=PASS status=0,duration=0" 68 | }, 69 | response: { 70 | status: 204 71 | } 72 | } 73 | }); 74 | 75 | addInteractionHandler('save test results with custom tags and fields to influx2', () => { 76 | return { 77 | request: { 78 | method: 'POST', 79 | path: '/api/v2/write', 80 | headers: { 81 | "authorization": "Token testtoken" 82 | }, 83 | queryParams: { 84 | "org": "testorg", 85 | "bucket": "testbucket", 86 | "precision": "ns" 87 | }, 88 | body: "TestRun,Team=QA,App=PactumJS,Name=Staging\\ -\\ UI\\ Smoke\\ Test\\ Run,Status=FAIL id=123,stringfield=\"coolvalue\",status=1,total=2,passed=1,failed=1,duration=1883597\n" + 89 | "TestSuite,Team=QA,App=PactumJS,Name=desktop-chrome,Status=PASS id=123,stringfield=\"coolvalue\",status=0,total=1,passed=1,failed=0,duration=1164451\n" + 90 | "TestCase,Team=QA,App=PactumJS,Name=GU,Status=PASS id=123,stringfield=\"coolvalue\",status=0,duration=243789\n" + 91 | "TestSuite,Team=QA,App=PactumJS,Name=mobile-andoid,Status=FAIL id=123,stringfield=\"coolvalue\",status=1,total=1,passed=0,failed=1,duration=714100\n" + 92 | "TestCase,Team=QA,App=PactumJS,Name=GU,Status=FAIL id=123,stringfield=\"coolvalue\",status=1,duration=156900" 93 | }, 94 | response: { 95 | status: 204 96 | } 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /test/mocks/percy.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('get percy project', () => { 4 | return { 5 | request: { 6 | method: 'GET', 7 | path: '/api/v1/projects', 8 | form: { 9 | 'project_slug': 'project-name' 10 | }, 11 | headers: { 12 | 'authorization': 'Token token' 13 | } 14 | }, 15 | response: { 16 | status: 200, 17 | body: { 18 | "data": { 19 | "id": "project-id", 20 | "attributes": { 21 | "full-slug": "org-uid/project-name", 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }); 28 | 29 | addInteractionHandler('get last build from percy', () => { 30 | return { 31 | request: { 32 | method: 'GET', 33 | path: '/api/v1/builds', 34 | queryParams: { 35 | 'project_id': 'project-id', 36 | 'page[limit]': '1' 37 | }, 38 | headers: { 39 | 'authorization': 'Token token' 40 | } 41 | }, 42 | response: { 43 | status: 200, 44 | body: { 45 | "data": [ 46 | { 47 | "id": "build-id", 48 | "attributes": { 49 | "state": "finished", 50 | "total-snapshots": 1, 51 | "total-snapshots-unreviewed": 0, 52 | } 53 | } 54 | ], 55 | "included": [ 56 | { 57 | "type": "projects", 58 | "id": "project-id", 59 | "attributes": { 60 | "name": "project-name", 61 | "full-slug": "org-uid/project-name", 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | }); 69 | 70 | addInteractionHandler('get last build with un-reviewed snapshots from percy', () => { 71 | return { 72 | request: { 73 | method: 'GET', 74 | path: '/api/v1/builds', 75 | queryParams: { 76 | 'project_id': 'project-id', 77 | 'page[limit]': '1' 78 | }, 79 | headers: { 80 | 'authorization': 'Token token' 81 | } 82 | }, 83 | response: { 84 | status: 200, 85 | body: { 86 | "data": [ 87 | { 88 | "id": "build-id", 89 | "attributes": { 90 | "state": "finished", 91 | "total-snapshots": 1, 92 | "total-snapshots-unreviewed": 1, 93 | } 94 | } 95 | ] 96 | } 97 | } 98 | } 99 | }); 100 | 101 | addInteractionHandler('get empty removed snapshots from percy', () => { 102 | return { 103 | request: { 104 | method: 'GET', 105 | path: '/api/v1/builds/build-id/removed-snapshots', 106 | headers: { 107 | 'authorization': 'Token token' 108 | } 109 | }, 110 | response: { 111 | status: 200, 112 | body: { 113 | "data": [] 114 | } 115 | } 116 | } 117 | }); 118 | 119 | addInteractionHandler('get removed snapshots from percy', () => { 120 | return { 121 | request: { 122 | method: 'GET', 123 | path: '/api/v1/builds/build-id/removed-snapshots', 124 | headers: { 125 | 'authorization': 'Token token' 126 | } 127 | }, 128 | response: { 129 | status: 200, 130 | body: { 131 | "data": [ 132 | { 133 | "type": "snapshots", 134 | "id": "", 135 | "attributes": { 136 | "name": "" 137 | }, 138 | "links": { 139 | "self": "/api/v1/snapshots/" 140 | } 141 | }, 142 | { 143 | "type": "snapshots", 144 | "id": "", 145 | "attributes": { 146 | "name": "" 147 | }, 148 | "links": { 149 | "self": "/api/v1/snapshots/" 150 | } 151 | } 152 | ] 153 | } 154 | } 155 | } 156 | }); 157 | 158 | addInteractionHandler('get build from percy', () => { 159 | return { 160 | request: { 161 | method: 'GET', 162 | path: '/api/v1/builds/build-id', 163 | headers: { 164 | 'authorization': 'Token token' 165 | } 166 | }, 167 | response: { 168 | status: 200, 169 | body: { 170 | "data": { 171 | "id": "build-id", 172 | "attributes": { 173 | "state": "finished", 174 | "total-snapshots": 1, 175 | "total-snapshots-unreviewed": 0, 176 | } 177 | }, 178 | "included": [ 179 | { 180 | "type": "projects", 181 | "id": "project-id", 182 | "attributes": { 183 | "name": "project-name", 184 | "full-slug": "org-uid/project-name", 185 | } 186 | } 187 | ] 188 | } 189 | } 190 | } 191 | }); -------------------------------------------------------------------------------- /test/mocks/rp.mock.js: -------------------------------------------------------------------------------- 1 | const { addInteractionHandler } = require('pactum').handler; 2 | 3 | addInteractionHandler('get launch details', () => { 4 | return { 5 | request: { 6 | method: 'GET', 7 | path: '/api/v1/project-name/launch/id123', 8 | headers: { 9 | "authorization": "Bearer abc" 10 | } 11 | }, 12 | response: { 13 | status: 200, 14 | body: { 15 | "id": 123, 16 | "uuid": "uuid", 17 | "statistics": { 18 | "defects": { 19 | "to_investigate": { 20 | "total": 4, 21 | "ti001": 4 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }); 29 | 30 | addInteractionHandler('get last launch details', () => { 31 | return { 32 | request: { 33 | method: 'GET', 34 | path: '/api/v1/project-name/launch', 35 | queryParams: { 36 | "filter.eq.name": "smoke", 37 | "page.size": "1", 38 | "page.sort": "startTime,desc" 39 | }, 40 | headers: { 41 | "authorization": "Bearer abc" 42 | } 43 | }, 44 | response: { 45 | status: 200, 46 | body: { 47 | "content": [ 48 | { 49 | "id": 123, 50 | "uuid": "uuid", 51 | "statistics": { 52 | "defects": { 53 | "to_investigate": { 54 | "total": 4, 55 | "ti001": 4 56 | } 57 | } 58 | } 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | }); 65 | 66 | addInteractionHandler('get suite history', () => { 67 | return { 68 | request: { 69 | method: 'GET', 70 | path: '/api/v1/project-name/item/history', 71 | queryParams: { 72 | "historyDepth": "5", 73 | "filter.eq.launchId": "123", 74 | "filter.!ex.parentId": "true" 75 | }, 76 | headers: { 77 | "authorization": "Bearer abc" 78 | } 79 | }, 80 | response: { 81 | status: 200, 82 | body: { 83 | "content": [ 84 | { 85 | "resources": [ 86 | { 87 | "uuid": "uuid", 88 | "status": "FAILED", 89 | }, 90 | { 91 | "uuid": "uuid", 92 | "status": "PASSED", 93 | }, 94 | { 95 | "uuid": "uuid", 96 | "status": "RUNNING", 97 | } 98 | ] 99 | } 100 | ] 101 | } 102 | } 103 | } 104 | }); 105 | 106 | addInteractionHandler('get empty suite history', () => { 107 | return { 108 | request: { 109 | method: 'GET', 110 | path: '/api/v1/project-name/item/history', 111 | queryParams: { 112 | "historyDepth": "5", 113 | "filter.eq.launchId": "123", 114 | "filter.!ex.parentId": "true" 115 | }, 116 | headers: { 117 | "authorization": "Bearer abc" 118 | } 119 | }, 120 | response: { 121 | status: 200, 122 | body: { 123 | "content": [ 124 | { 125 | "resources": [] 126 | } 127 | ] 128 | } 129 | } 130 | } 131 | }); -------------------------------------------------------------------------------- /test/publish.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | xdescribe('publish - testng', () => { 6 | 7 | it('test-summary for single suite - teams', async () => { 8 | const id = mock.addInteraction('post test-summary to teams with single suite'); 9 | await publish({ config: 'test/data/configs/testng.single-suite.json' }); 10 | assert.equal(mock.getInteraction(id).exercised, true); 11 | }); 12 | 13 | it('test-summary for multiple suites - teams and slack', async () => { 14 | const id1 = mock.addInteraction('post test-summary to teams with multiple suites'); 15 | const id2 = mock.addInteraction('post test-summary to slack with multiple suites'); 16 | const id3 = mock.addInteraction('post failure-details to teams with multiple suites'); 17 | const id4 = mock.addInteraction('post failure-details to slack with multiple suites'); 18 | await publish({ config: 'test/data/configs/testng.multiple-suites.json' }); 19 | assert.equal(mock.getInteraction(id1).exercised, true); 20 | assert.equal(mock.getInteraction(id2).exercised, true); 21 | assert.equal(mock.getInteraction(id3).exercised, true); 22 | assert.equal(mock.getInteraction(id4).exercised, true); 23 | }); 24 | 25 | it('test-summary for single suite - teams with slim report', async () => { 26 | const id = mock.addInteraction('post test-summary to teams with single suite'); 27 | await publish({ config: 'test/data/configs/testng.single-suite.slim.json' }); 28 | assert.equal(mock.getInteraction(id).exercised, true); 29 | }); 30 | 31 | it('test-summary for multiple suites - teams and slack with slim report', async () => { 32 | const id1 = mock.addInteraction('post test-summary-slim to teams with multiple suites'); 33 | const id2 = mock.addInteraction('post test-summary-slim to slack with multiple suites'); 34 | await publish({ config: 'test/data/configs/testng.multiple-suites.slim.json' }); 35 | assert.equal(mock.getInteraction(id1).exercised, true); 36 | assert.equal(mock.getInteraction(id2).exercised, true); 37 | }); 38 | 39 | it('failure-details for single suite with all tests passed - slack and teams', async () => { 40 | await publish({ config: 'test/data/configs/testng.single-suite.pass.json' }); 41 | }); 42 | 43 | it('failure-details for single suite with failures - slack and teams', async () => { 44 | const id1 = mock.addInteraction('post failure-details to teams with single suite'); 45 | const id2 = mock.addInteraction('post failure-details to slack with single suite'); 46 | await publish({ config: 'test/data/configs/testng.single-suite.fail.json' }); 47 | assert.equal(mock.getInteraction(id1).exercised, true); 48 | assert.equal(mock.getInteraction(id2).exercised, true); 49 | }); 50 | 51 | it('test-summary to teams and slack with retries', async () => { 52 | const id1 = mock.addInteraction('post test-summary to teams with retries'); 53 | const id2 = mock.addInteraction('post test-summary to slack with retries'); 54 | await publish({ config: 'test/data/configs/testng.multiple-suites-retries.json' }); 55 | assert.equal(mock.getInteraction(id1).exercised, true); 56 | assert.equal(mock.getInteraction(id2).exercised, true); 57 | }); 58 | 59 | afterEach(() => { 60 | mock.clearInteractions(); 61 | }); 62 | 63 | }); 64 | 65 | xdescribe('publish - junit', () => { 66 | 67 | it('test-summary for single suite - teams & slack', async () => { 68 | const id1 = mock.addInteraction('post test-summary to teams with single suite'); 69 | const id2 = mock.addInteraction('post test-summary to slack with single suite'); 70 | await publish({ config: 'test/data/configs/junit.single-suite.json' }); 71 | assert.equal(mock.getInteraction(id1).exercised, true); 72 | assert.equal(mock.getInteraction(id2).exercised, true); 73 | }); 74 | 75 | it('failure-*-* for single suite no failures', async () => { 76 | await publish({ config: 'test/data/configs/failure-all-reports-no-failures.json' }); 77 | }); 78 | 79 | it('failure-*-* for single suite failures', async () => { 80 | const id1 = mock.addInteraction('post failure-summary to teams'); 81 | const id2 = mock.addInteraction('post failure-summary to slack'); 82 | const id3 = mock.addInteraction('post failure-summary-slim to teams'); 83 | const id4 = mock.addInteraction('post failure-summary-slim to slack'); 84 | const id5 = mock.addInteraction('post failure-details-slim to teams'); 85 | const id6 = mock.addInteraction('post failure-details-slim to slack'); 86 | await publish({ config: 'test/data/configs/failure-all-reports-failures.json' }); 87 | assert.equal(mock.getInteraction(id1).exercised, true); 88 | assert.equal(mock.getInteraction(id2).exercised, true); 89 | assert.equal(mock.getInteraction(id3).exercised, true); 90 | assert.equal(mock.getInteraction(id4).exercised, true); 91 | assert.equal(mock.getInteraction(id5).exercised, true); 92 | assert.equal(mock.getInteraction(id6).exercised, true); 93 | }); 94 | 95 | afterEach(() => { 96 | mock.clearInteractions(); 97 | }); 98 | 99 | }); 100 | 101 | xdescribe('publish - custom', () => { 102 | 103 | it('custom target', async () => { 104 | const id1 = mock.addInteraction('get custom'); 105 | await publish({ config: 'test/data/configs/custom-target.json' }); 106 | assert.equal(mock.getInteraction(id1).exercised, true); 107 | }); 108 | 109 | afterEach(() => { 110 | mock.clearInteractions(); 111 | }); 112 | 113 | }); 114 | 115 | xdescribe('publish - report portal analysis', () => { 116 | 117 | it('test-summary for single suite - teams', async () => { 118 | const id1 = mock.addInteraction('get launch details'); 119 | const id2 = mock.addInteraction('post test-summary to teams with report portal analysis'); 120 | const id3 = mock.addInteraction('post test-summary to slack with report portal analysis'); 121 | await publish({ config: 'test/data/configs/report-portal-analysis.json' }); 122 | assert.equal(mock.getInteraction(id1).exercised, true); 123 | assert.equal(mock.getInteraction(id2).exercised, true); 124 | assert.equal(mock.getInteraction(id3).exercised, true); 125 | }); 126 | 127 | afterEach(() => { 128 | mock.clearInteractions(); 129 | }); 130 | 131 | }); -------------------------------------------------------------------------------- /test/results.custom.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('results - custom - functional', () => { 6 | 7 | it('should send test-summary', async () => { 8 | const id = mock.addInteraction('post test-summary to slack'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "slack", 14 | "inputs": { 15 | "url": "http://localhost:9393/message" 16 | } 17 | } 18 | ], 19 | "results": [ 20 | { 21 | "type": "custom", 22 | "result": { 23 | id: '', 24 | name: 'Default suite', 25 | total: 4, 26 | passed: 4, 27 | failed: 0, 28 | errors: 0, 29 | skipped: 0, 30 | retried: 0, 31 | duration: 2000, 32 | status: 'PASS', 33 | suites: [ 34 | { 35 | id: '', 36 | name: 'Default test', 37 | total: 4, 38 | passed: 4, 39 | failed: 0, 40 | errors: 0, 41 | skipped: 0, 42 | duration: 2000, 43 | status: 'PASS', 44 | cases: [ 45 | { 46 | id: '', 47 | name: 'c2', 48 | total: 0, 49 | passed: 0, 50 | failed: 0, 51 | errors: 0, 52 | skipped: 0, 53 | duration: 0, 54 | status: 'PASS', 55 | failure: '', 56 | stack_trace: '', 57 | steps: [] 58 | }, 59 | { 60 | id: '', 61 | name: 'c3', 62 | total: 0, 63 | passed: 0, 64 | failed: 0, 65 | errors: 0, 66 | skipped: 0, 67 | duration: 10, 68 | status: 'PASS', 69 | failure: '', 70 | stack_trace: '', 71 | steps: [] 72 | }, 73 | { 74 | id: '', 75 | name: 'c1', 76 | total: 0, 77 | passed: 0, 78 | failed: 0, 79 | errors: 0, 80 | skipped: 0, 81 | duration: 0, 82 | status: 'PASS', 83 | failure: '', 84 | stack_trace: '', 85 | steps: [] 86 | }, 87 | { 88 | id: '', 89 | name: 'c4', 90 | total: 0, 91 | passed: 0, 92 | failed: 0, 93 | errors: 0, 94 | skipped: 0, 95 | duration: 0, 96 | status: 'PASS', 97 | failure: 'expected [true] but found [false]', 98 | stack_trace: '', 99 | steps: [] 100 | } 101 | ] 102 | } 103 | ] 104 | } 105 | } 106 | ] 107 | } 108 | }); 109 | assert.equal(mock.getInteraction(id).exercised, true); 110 | }); 111 | 112 | afterEach(() => { 113 | mock.clearInteractions(); 114 | }); 115 | 116 | }); -------------------------------------------------------------------------------- /test/results.junit.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/test/results.junit.spec.js -------------------------------------------------------------------------------- /test/results.testng.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/test/results.testng.spec.js -------------------------------------------------------------------------------- /test/results.xunit.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/test-results-reporter/testbeats/261a504006bd0c13e54eb352cfcc78c8a54cf1b1/test/results.xunit.spec.js -------------------------------------------------------------------------------- /test/targets.custom.spec.js: -------------------------------------------------------------------------------- 1 | const { mock, spec } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('targets - custom', () => { 6 | 7 | it('load from fs', async () => { 8 | const id1 = mock.addInteraction('get custom'); 9 | await publish({ 10 | config: 'test/data/configs/custom-target.json' 11 | }); 12 | assert.equal(mock.getInteraction(id1).exercised, true); 13 | }); 14 | 15 | it('load from inline function', async () => { 16 | const id1 = mock.addInteraction('get custom'); 17 | await publish({ 18 | config: { 19 | "targets": [ 20 | { 21 | "name": "custom", 22 | "inputs": { 23 | "load": async function ({ target, result }) { 24 | assert.equal(target.name, 'custom'); 25 | assert.equal(result.name, 'Default suite'); 26 | await spec().get('http://localhost:9393/custom'); 27 | } 28 | } 29 | } 30 | ], 31 | "results": [ 32 | { 33 | "type": "junit", 34 | "files": [ 35 | "test/data/junit/single-suite.xml" 36 | ] 37 | } 38 | ] 39 | } 40 | }); 41 | assert.equal(mock.getInteraction(id1).exercised, true); 42 | }); 43 | 44 | it('invalid load', async () => { 45 | let err; 46 | try { 47 | await publish({ 48 | config: { 49 | "targets": [ 50 | { 51 | "name": "custom", 52 | "inputs": { 53 | "load": {} 54 | } 55 | } 56 | ], 57 | "results": [ 58 | { 59 | "type": "junit", 60 | "files": [ 61 | "test/data/junit/single-suite.xml" 62 | ] 63 | } 64 | ] 65 | } 66 | }); 67 | } catch (error) { 68 | err = error 69 | } 70 | assert.equal(err, `Invalid 'load' input in custom target - [object Object]`); 71 | }); 72 | 73 | afterEach(() => { 74 | mock.clearInteractions(); 75 | }); 76 | 77 | }); -------------------------------------------------------------------------------- /test/targets.delay.spec.js: -------------------------------------------------------------------------------- 1 | const { publish } = require('../src'); 2 | 3 | describe('targets - delay', () => { 4 | 5 | it('delay for sometime', async () => { 6 | await publish({ 7 | config: { 8 | "targets": [ 9 | { 10 | "name": "delay", 11 | "inputs": { 12 | "seconds": 0.00001 13 | } 14 | } 15 | ], 16 | "results": [ 17 | { 18 | "type": "testng", 19 | "files": [ 20 | "test/data/testng/single-suite.xml" 21 | ] 22 | } 23 | ] 24 | } 25 | }); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /test/targets.influx.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('targets - influx - performance', () => { 6 | 7 | it('should save results', async () => { 8 | const id = mock.addInteraction('save perf results'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "influx", 14 | "inputs": { 15 | "url": "http://localhost:9393", 16 | "db": "TestResults", 17 | "username": "user", 18 | "password": "pass" 19 | } 20 | } 21 | ], 22 | "results": [ 23 | { 24 | "type": "jmeter", 25 | "files": [ 26 | "test/data/jmeter/sample.csv" 27 | ] 28 | } 29 | ] 30 | } 31 | }); 32 | assert.equal(mock.getInteraction(id).exercised, true); 33 | }); 34 | 35 | it('should save results with custom tags and fields', async () => { 36 | const id = mock.addInteraction('save perf results with custom tags and fields'); 37 | await publish({ 38 | config: { 39 | "targets": [ 40 | { 41 | "name": "influx", 42 | "inputs": { 43 | "url": "http://localhost:9393", 44 | "db": "TestResults", 45 | "username": "user", 46 | "password": "pass", 47 | "tags": { 48 | "Team": "QA", 49 | "App": "PactumJS" 50 | }, 51 | "fields": { 52 | "id": 123, 53 | } 54 | } 55 | } 56 | ], 57 | "results": [ 58 | { 59 | "type": "jmeter", 60 | "files": [ 61 | "test/data/jmeter/sample.csv" 62 | ] 63 | } 64 | ] 65 | } 66 | }); 67 | assert.equal(mock.getInteraction(id).exercised, true); 68 | }); 69 | 70 | afterEach(() => { 71 | mock.clearInteractions(); 72 | }); 73 | 74 | }); 75 | 76 | describe('targets - influx - functional', () => { 77 | 78 | it('should save results', async () => { 79 | const id = mock.addInteraction('save test results'); 80 | await publish({ 81 | config: { 82 | "targets": [ 83 | { 84 | "name": "influx", 85 | "inputs": { 86 | "url": "http://localhost:9393", 87 | "db": "TestResults", 88 | "username": "user", 89 | "password": "pass" 90 | } 91 | } 92 | ], 93 | "results": [ 94 | { 95 | "type": "testng", 96 | "files": [ 97 | "test/data/testng/single-suite.xml" 98 | ] 99 | } 100 | ] 101 | } 102 | }); 103 | assert.equal(mock.getInteraction(id).exercised, true); 104 | }); 105 | 106 | it('should save results with custom tags and fields', async () => { 107 | const id = mock.addInteraction('save test results with custom tags and fields'); 108 | await publish({ 109 | config: { 110 | "targets": [ 111 | { 112 | "name": "influx", 113 | "inputs": { 114 | "url": "http://localhost:9393", 115 | "db": "TestResults", 116 | "username": "user", 117 | "password": "pass", 118 | "tags": { 119 | "Team": "QA", 120 | "App": "PactumJS" 121 | }, 122 | "fields": { 123 | "id": 123, 124 | } 125 | } 126 | } 127 | ], 128 | "results": [ 129 | { 130 | "type": "testng", 131 | "files": [ 132 | "test/data/testng/multiple-suites-failures.xml" 133 | ] 134 | } 135 | ] 136 | } 137 | }); 138 | assert.equal(mock.getInteraction(id).exercised, true); 139 | }); 140 | 141 | afterEach(() => { 142 | mock.clearInteractions(); 143 | }); 144 | 145 | }); -------------------------------------------------------------------------------- /test/targets.influx2.spec.js: -------------------------------------------------------------------------------- 1 | const { mock, handler } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('targets - influx2 - performance', () => { 6 | 7 | it('should save results', async () => { 8 | const id = mock.addInteraction('save perf results to influx2'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "influx", 14 | "inputs": { 15 | "url": "http://localhost:9393", 16 | "version": "v2", 17 | "token": "testtoken", 18 | "org": "testorg", 19 | "bucket": "testbucket", 20 | "precision": "ns", 21 | } 22 | } 23 | ], 24 | "results": [ 25 | { 26 | "type": "jmeter", 27 | "files": [ 28 | "test/data/jmeter/sample.csv" 29 | ] 30 | } 31 | ] 32 | } 33 | }); 34 | assert.equal(mock.getInteraction(id).exercised, true); 35 | }); 36 | 37 | it('should save results with custom tags and fields', async () => { 38 | const id = mock.addInteraction('save perf results with custom tags and fields to influx2'); 39 | await publish({ 40 | config: { 41 | "targets": [ 42 | { 43 | "name": "influx", 44 | "inputs": { 45 | "url": "http://localhost:9393", 46 | "version": "v2", 47 | "token": "testtoken", 48 | "org": "testorg", 49 | "bucket": "testbucket", 50 | "precision": "ns", 51 | "tags": { 52 | "Team": "QA", 53 | "App": "PactumJS" 54 | }, 55 | "fields": { 56 | "id": 123, 57 | } 58 | } 59 | } 60 | ], 61 | "results": [ 62 | { 63 | "type": "jmeter", 64 | "files": [ 65 | "test/data/jmeter/sample.csv" 66 | ] 67 | } 68 | ] 69 | } 70 | }); 71 | 72 | assert.equal(mock.getInteraction(id).exercised, true); 73 | }); 74 | 75 | afterEach(() => { 76 | mock.clearInteractions(); 77 | }); 78 | 79 | }); 80 | 81 | describe('targets - influx2 - functional', () => { 82 | 83 | it('should save results', async () => { 84 | const id = mock.addInteraction('save test results to influx2'); 85 | await publish({ 86 | config: { 87 | "targets": [ 88 | { 89 | "name": "influx", 90 | "inputs": { 91 | "url": "http://localhost:9393", 92 | "version": "v2", 93 | "token": "testtoken", 94 | "org": "testorg", 95 | "bucket": "testbucket", 96 | "precision": "ns", 97 | } 98 | } 99 | ], 100 | "results": [ 101 | { 102 | "type": "testng", 103 | "files": [ 104 | "test/data/testng/single-suite.xml" 105 | ] 106 | } 107 | ] 108 | } 109 | }); 110 | assert.equal(mock.getInteraction(id).exercised, true); 111 | }); 112 | 113 | it('should save results with custom tags and fields', async () => { 114 | const id = mock.addInteraction('save test results with custom tags and fields to influx2'); 115 | await publish({ 116 | config: { 117 | "targets": [ 118 | { 119 | "name": "influx", 120 | "inputs": { 121 | "url": "http://localhost:9393", 122 | "version": "v2", 123 | "token": "testtoken", 124 | "org": "testorg", 125 | "bucket": "testbucket", 126 | "precision": "ns", 127 | "tags": { 128 | "Team": "QA", 129 | "App": "PactumJS" 130 | }, 131 | "fields": { 132 | "id": 123, 133 | "stringfield": "coolvalue" 134 | } 135 | } 136 | } 137 | ], 138 | "results": [ 139 | { 140 | "type": "testng", 141 | "files": [ 142 | "test/data/testng/multiple-suites-failures.xml" 143 | ] 144 | } 145 | ] 146 | } 147 | }); 148 | assert.equal(mock.getInteraction(id).exercised, true); 149 | }); 150 | 151 | afterEach(() => { 152 | mock.clearInteractions(); 153 | }); 154 | 155 | }); -------------------------------------------------------------------------------- /test/targets.spec.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('pactum'); 2 | const assert = require('assert'); 3 | const { publish } = require('../src'); 4 | 5 | describe('Targets', () => { 6 | 7 | it('disable target', async () => { 8 | const id = mock.addInteraction('post test-summary to slack'); 9 | await publish({ 10 | config: { 11 | "targets": [ 12 | { 13 | "name": "slack", 14 | "inputs": { 15 | "url": "http://localhost:9393/message" 16 | } 17 | }, 18 | { 19 | name: 'teams', 20 | enable: false 21 | } 22 | ], 23 | "results": [ 24 | { 25 | "type": "testng", 26 | "files": [ 27 | "test/data/testng/single-suite.xml" 28 | ] 29 | } 30 | ] 31 | } 32 | }); 33 | assert.equal(mock.getInteraction(id).exercised, true); 34 | }); 35 | 36 | it('disable extension', async () => { 37 | const id = mock.addInteraction('post test-summary to slack'); 38 | await publish({ 39 | config: { 40 | "targets": [ 41 | { 42 | "name": "slack", 43 | "inputs": { 44 | "url": "http://localhost:9393/message" 45 | } 46 | } 47 | ], 48 | "extensions": [ 49 | { 50 | name: 'ci-info', 51 | enable: false 52 | } 53 | ], 54 | "results": [ 55 | { 56 | "type": "testng", 57 | "files": [ 58 | "test/data/testng/single-suite.xml" 59 | ] 60 | } 61 | ] 62 | } 63 | }); 64 | assert.equal(mock.getInteraction(id).exercised, true); 65 | }); 66 | }); --------------------------------------------------------------------------------