├── .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 | 
6 |
7 |
8 | #### Publish test results to Microsoft Teams, Google Chat, Slack and many more.
9 |
10 |
11 |
12 | [](https://github.com/test-results-reporter/testbeats/actions/workflows/build.yml)
13 | [](https://www.npmjs.com/package/test-results-parser)
14 | [](https://bundlephobia.com/result?p=testbeats)
15 |
16 |
17 | [](https://github.com/test-results-reporter/testbeats/stargazers)
18 | 
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 | 
37 |
38 | #### Results in Portal
39 |
40 | 
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 | });
--------------------------------------------------------------------------------