@testmonitor/testmonitor-cli 0.0.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +15 -0
- package/.prettierignore +10 -0
- package/CHANGELOG.md +9 -0
- package/README.md +46 -57
- package/dist/index.js +2 -0
- package/package.json +55 -42
- package/src/commands/junit/index.js +12 -0
- package/src/commands/junit/submit-junit-xml-command.js +84 -0
- package/src/index.js +23 -0
- package/src/lib/api-client.js +104 -0
- package/src/lib/app-version.js +8 -0
- package/src/lib/logger.js +59 -0
- package/actions/SubmitReport.js +0 -83
- package/commands/submit.js +0 -64
- package/index.js +0 -20
package/.editorconfig
ADDED
package/.prettierignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2025-08-15
|
|
8
|
+
### Added
|
|
9
|
+
- Initial version.
|
package/README.md
CHANGED
|
@@ -1,95 +1,84 @@
|
|
|
1
1
|
# TestMonitor CLI
|
|
2
2
|
|
|
3
|
-
TestMonitor CLI (`testmonitor-cli`)
|
|
3
|
+
The TestMonitor CLI (`testmonitor-cli`) lets you interact with the TestMonitor platform directly from your terminal or CI pipelines. You can use it to submit automated test results through a simple, scriptable interface.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Getting Started](#getting-started)
|
|
9
|
+
- [Command Reference](#command-reference)
|
|
10
|
+
- [Documentation](#documention)
|
|
11
|
+
- [License](#license)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
npm install -g testmonitor-cli
|
|
11
|
-
```
|
|
13
|
+
## Installation
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
Install the CLI globally using NPM:
|
|
14
16
|
|
|
15
17
|
```sh
|
|
16
|
-
|
|
18
|
+
npm install -g @testmonitor/testmonitor-cli
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## Authentication
|
|
22
|
-
|
|
23
|
-
TestMonitor CLI requires authentication via an API token. Set the token as an environment variable:
|
|
21
|
+
To upgrade to the latest version later on, run:
|
|
24
22
|
|
|
25
23
|
```sh
|
|
26
|
-
|
|
24
|
+
npm update -g @testmonitor/testmonitor-cli
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
After installation, you can verify the installed version with:
|
|
30
28
|
|
|
31
29
|
```sh
|
|
32
|
-
|
|
30
|
+
testmonitor-cli --version
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
Once you've setup your TestMonitor token, you can start submitting JUnit reports:
|
|
33
|
+
## Getting Started
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
testmonitor-cli submit -d <TESTMONITOR_DOMAIN> -i <INTEGRATION_ID> -f <FILE_PATH>
|
|
41
|
-
```
|
|
35
|
+
To begin using the CLI, follow these steps:
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
1. Go to your project settings in TestMonitor.
|
|
38
|
+
2. Enable the **JUnit XML** integration under the **Integrations** menu.
|
|
39
|
+
3. Copy the **Token**.
|
|
40
|
+
4. Run the following command:
|
|
44
41
|
|
|
45
42
|
```sh
|
|
46
|
-
testmonitor-cli submit -d domain.testmonitor.com -
|
|
43
|
+
testmonitor-cli junit submit -d domain.testmonitor.com -t <token> -f ./junit.xml
|
|
47
44
|
```
|
|
45
|
+
> Replace `<token>` with your actual integration ID.
|
|
48
46
|
|
|
49
|
-
##
|
|
47
|
+
## Command Reference
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
This section lists all available commands grouped by functionality.
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
### JUnit
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
```sh
|
|
57
|
-
-d, --domain <domain> TestMonitor domain (required)
|
|
58
|
-
-i, --integration-id <id> Integration ID (required)
|
|
59
|
-
-f, --file <path> Path to JUnit XML file (required)
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Development
|
|
53
|
+
Commands for managing JUnit-style test reports.
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
#### Submit
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
git clone https://github.com/testmonitor/testmonitor-cli.git
|
|
68
|
-
cd testmonitor-cli
|
|
69
|
-
npm install
|
|
70
|
-
```
|
|
57
|
+
Submit a JUnit XML report and create a new test run in your TestMonitor project.
|
|
71
58
|
|
|
72
|
-
Run locally:
|
|
73
59
|
```sh
|
|
74
|
-
|
|
60
|
+
testmonitor-cli junit submit [options]
|
|
75
61
|
```
|
|
76
62
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
| Option | Description |
|
|
64
|
+
|-----------------------------------|--------------------------------------------------|
|
|
65
|
+
| -d, --domain <domain> | TestMonitor domain (required) |
|
|
66
|
+
| -t, --token <token> | JUnit token (required) |
|
|
67
|
+
| -f, --file <path> | Path to JUnit XML file (required) |
|
|
68
|
+
| -a, --automation-type <type> | Test type (e.g. playwright, selenium) |
|
|
69
|
+
| -n, --name <name> | Custom test run name |
|
|
70
|
+
| -m, --milestone-id <id> | Target milestone ID |
|
|
71
|
+
| -o, --milestone-name <name> | Milestone name to find or create |
|
|
72
|
+
| -e, --test-environment-id <id> | Target test environment ID |
|
|
73
|
+
| -p, --preserve-names | Preserve original test case names |
|
|
74
|
+
| -s, --skip-root-suite | Skip root test suite nesting |
|
|
75
|
+
| -v, --verbose | Output debug information |
|
|
86
76
|
|
|
87
|
-
##
|
|
77
|
+
## Documentation
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
* **Stephan Grootveld** - *Developer* - [Stefanius](https://github.com/stefanius)
|
|
91
|
-
* **Frank Keulen** - *Developer* - [FrankIsGek](https://github.com/frankisgek)
|
|
79
|
+
Please refer to our [knowledge base](https://help.testmonitor.com/) for more information.
|
|
92
80
|
|
|
93
81
|
## License
|
|
94
82
|
|
|
95
|
-
|
|
83
|
+
Copyright (c) TestMonitor | we are Cerios B.V.
|
|
84
|
+
All rights reserved.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var N=Object.defineProperty;var D=(n,e)=>()=>(n&&(e=n(n=0)),e);var S=(n,e)=>{for(var t in e)N(n,t,{get:e[t],enumerable:!0})};var b={};S(b,{default:()=>L});var L,T=D(()=>{L={name:"@testmonitor/testmonitor-cli",type:"module",version:"0.0.2",description:"The TestMonitor CLI lets you interact with the TestMonitor platform directly from your terminal or CI pipelines.",keywords:["testmonitor","cli"],homepage:"https://www.testmonitor.com/",bugs:{url:"https://github.com/testmonitor/testmonitor-cli/issues"},repository:{type:"git",url:"git+https://github.com/testmonitor/testmonitor-cli.git"},author:"TestMonitor | we are Cerios B.V.",main:"src/index.js",bin:{"testmonitor-cli":"dist/index.js"},scripts:{start:"node src/index.js",build:"node esbuild.config.js",lint:"eslint .","lint:fix":"eslint . --fix",test:"NODE_OPTIONS='--experimental-vm-modules' jest","test:watch":"NODE_OPTIONS='--experimental-vm-modules' jest --watch","test:coverage":"NODE_OPTIONS='--experimental-vm-modules' jest --coverage"},dependencies:{axios:"^1.7.9",chalk:"^5.4.1",commander:"^14.0.0",dotenv:"^17.2.0",ora:"^8.2.0","strip-ansi":"^7.1.0",table:"^6.9.0"},devDependencies:{chai:"^5.2.0",esbuild:"^0.25.6",eslint:"^9.31.0","eslint-config-prettier":"^10.1.5","eslint-formatter-junit":"^8.40.0","eslint-plugin-import":"^2.32.0","eslint-plugin-jest":"^28.14.0","eslint-plugin-jsdoc":"^51.4.1",jest:"^29.7.0","jest-junit":"^16.0.0",prettier:"^3.6.2"},engines:{node:">=18.0.0"}}});import"dotenv/config";import{Command as W}from"commander";async function y(){return"0.0.2"}import{Command as B}from"commander";import{Command as X}from"commander";import C from"os";import R from"fs";import P from"axios";import O from"form-data";var V=await y(),q=`NodeJS/${process.nodeVersion}`,J=`${C.platform()} ${C.release()}`,A=`TestMonitorCLI/${V} (${q}; ${J}; +https://www.testmonitor.com/)`,u=class{constructor({domain:e,apiKey:t,httpClient:o}){this.baseURL=`https://${e}/api/v1`,this.apiKey=t,this.httpClient=o||P.create({baseURL:this.baseURL,headers:{Authorization:`Bearer ${this.apiKey}`,"User-Agent":A}})}async get(e,t={}){return this._request("get",e,null,t)}async post(e,t={},o={}){return this._request("post",e,t,o)}async put(e,t={},o={}){return this._request("put",e,t,o)}async delete(e,t={}){return this._request("delete",e,null,t)}async _request(e,t,o,i){let s={...o instanceof O?o.getHeaders():{"Content-Type":"application/json"},...i.headers||{}};try{return(await this.httpClient.request({method:e,url:t,data:o,headers:s,...i})).data}catch(a){let f=a.response?.data?.message||a.message;throw new Error(f)}}async checkConnection(){return this.get("/my-account")}async submitJUnitReport({integrationId:e,filePath:t,name:o,type:i,milestoneId:r,milestoneName:s,preserveNames:a,skipRootSuite:f,testEnvironmentId:w}){let c=new O;c.append("integration_id",e),c.append("report",R.createReadStream(t));let v={name:o,type:i,milestone:s,milestone_id:r,preserve_names:a?1:null,skip_root_suite:f?1:null,test_environment_id:w};for(let[m,l]of Object.entries(v))l!=null&&c.append(m,l);return this.post("/reporters/junit/submit",c)}};import d from"chalk";import K from"ora";import{table as F}from"table";var h=class{#e="TestMonitor";info(e){console.log(`${this.#e}: ${e}`)}success(e){console.log(`${this.#e}: ${d.green(e)}`)}warn(e){console.warn(`${this.#e}: ${d.yellow(e)}`)}error(e,t){let o=`${this.#e}: ${d.red(e)}`;console.error(o),process.env.DEBUG&&t instanceof Error&&console.error(d.gray(t.stack||t.message))}debug(e){process.env.DEBUG&&console.log(`${this.#e}: ${e}`)}line(e=""){console.log(e)}async spin(e,t,o={}){let i=K({text:`${this.#e}: ${e}`,color:"green"}).start();try{let r=await t();return i.succeed(`${this.#e}: ${d.green(o.successMessage||e)}`),r}catch(r){throw i.fail(`${this.#e}: ${d.red(o.errorMessage||e)}`),r}}table(e,t){let o=F(e,{singleLine:!0,...t});console.log(o)}};async function G(n,{client:e}={}){let{domain:t}=n,o=new h,i=process.env.TESTMONITOR_TOKEN;i||(o.error("TESTMONITOR_TOKEN environment variable is not set."),process.exit(1));let r=e||new u({domain:t,apiKey:i});try{let{data:s}=await o.spin("Connecting...",()=>r.checkConnection(),{successMessage:"Connection successful.",errorMessage:"Failed to connect."});o.line(),o.table([["User ID",s.id],["Name",s.name],["Email",s.email]])}catch(s){let a=s.response?.data?.message||s.message;o.line(),o.error(a,s),process.exit(1)}}var _=new X("auth").description("Verify connectivity with your TestMonitor instance.").requiredOption("-d, --domain <domain>","Your TestMonitor domain name (e.g., mydomain.testmonitor.com)").action(async n=>{await G(n)});var x=new B("check").name("check").description("Commands for verifying the connection state.").helpOption("-h, --help","Display help for command.");x.addCommand(_);var M=x;import{Command as Q}from"commander";import"dotenv/config";import Y from"fs";import{Command as z}from"commander";async function H(n,{client:e}={}){let{domain:t,integrationId:o,file:i,name:r,milestoneId:s,milestoneName:a,preserveNames:f,skipRootSuite:w,testEnvironmentId:c,type:v}=n,m=new h,l=process.env.TESTMONITOR_TOKEN;l||(m.error("TESTMONITOR_TOKEN environment variable is not set."),process.exit(1)),Y.existsSync(i)||(m.error(`File not found at path ${i}`),process.exit(1));let j=e||new u({domain:t,apiKey:l});try{let{data:p}=await m.spin("Submitting...",()=>j.submitJUnitReport({integrationId:o,filePath:i,name:r,type:v,milestoneId:s,milestoneName:a,preserveNames:f,skipRootSuite:w,testEnvironmentId:c}),{successMessage:"Report submitted.",errorMessage:"Failed to submit."});m.line(),m.table([["JUnit file",p.name],["Test run code",p.test_run.code],["Test run URL",p.test_run.links.show]])}catch(p){let k=p.response?.data?.message||p.message;m.line(),m.error(k,p),process.exit(1)}}var $=new z("submit").name("submit").description("Submit a JUnit XML file to your TestMonitor instance.").helpOption("-h, --help","Display help for command.").requiredOption("-d, --domain <domain>","Your TestMonitor domain name (e.g., mydomain.testmonitor.com)").requiredOption("-i, --integration-id <id>","The JUnit integration ID").requiredOption("-f, --file <path>","Path to your JUnit XML file").option("-m, --milestone-id <id>","Milestone ID for the new test run (required if not set in configuration))").option("-o, --milestone-name <name>","Find or create a milestone by name for the test run.").option("-n, --name <name>","Custom name for the test run (auto-generated if omitted).").option("-p, --preserve-names","Preserve test case names exactly as in the XML file.").option("-s, --skip-root-suite","Skip the root suite and start from nested suites.").option("-e, --test-environment-id <id>","Test environment ID for the new test run.").option("-t, --type <type>","The automation test type (cypress, playwright, phpunit, selenium).").option("-v, --verbose","Enable verbose debug output.").action(n=>H(n));var I=new Q("junit").name("junit").description("Commands for JUnit XML report submission.").helpOption("-h, --help","Display help for command.");I.addCommand($);var E=I;var g=new W;g.name("testmonitor-cli").version(await y(),"-v, --version","Display the current CLI version.").description("CLI tool for interacting with the TestMonitor API.").helpCommand(!1).helpOption("-h, --help","Display help for command.");g.on("option:verbose",function(){process.env.VERBOSE=this.opts().verbose});g.addCommand(M);g.addCommand(E);g.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,44 +1,57 @@
|
|
|
1
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
2
|
+
"name": "@testmonitor/testmonitor-cli",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"description": "The TestMonitor CLI lets you interact with the TestMonitor platform directly from your terminal or CI pipelines.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"testmonitor",
|
|
8
|
+
"cli"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://www.testmonitor.com/",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/testmonitor/testmonitor-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/testmonitor/testmonitor-cli.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "TestMonitor | we are Cerios B.V.",
|
|
19
|
+
"main": "src/index.js",
|
|
20
|
+
"bin": {
|
|
21
|
+
"testmonitor-cli": "dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node src/index.js",
|
|
25
|
+
"build": "node esbuild.config.js",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"lint:fix": "eslint . --fix",
|
|
28
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
29
|
+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
|
|
30
|
+
"test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"axios": "^1.7.9",
|
|
34
|
+
"chalk": "^5.4.1",
|
|
35
|
+
"commander": "^14.0.0",
|
|
36
|
+
"dotenv": "^17.2.0",
|
|
37
|
+
"ora": "^8.2.0",
|
|
38
|
+
"strip-ansi": "^7.1.0",
|
|
39
|
+
"table": "^6.9.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"chai": "^5.2.0",
|
|
43
|
+
"esbuild": "^0.25.6",
|
|
44
|
+
"eslint": "^9.31.0",
|
|
45
|
+
"eslint-config-prettier": "^10.1.5",
|
|
46
|
+
"eslint-formatter-junit": "^8.40.0",
|
|
47
|
+
"eslint-plugin-import": "^2.32.0",
|
|
48
|
+
"eslint-plugin-jest": "^28.14.0",
|
|
49
|
+
"eslint-plugin-jsdoc": "^51.4.1",
|
|
50
|
+
"jest": "^29.7.0",
|
|
51
|
+
"jest-junit": "^16.0.0",
|
|
52
|
+
"prettier": "^3.6.2"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
}
|
|
44
57
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
import { submitJUnitXMLCommand } from './submit-junit-xml-command.js';
|
|
4
|
+
|
|
5
|
+
const junitGroup = new Command('junit')
|
|
6
|
+
.name('junit')
|
|
7
|
+
.description('Commands for JUnit XML report submission.')
|
|
8
|
+
.helpOption('-h, --help', 'Display help for command.');
|
|
9
|
+
|
|
10
|
+
junitGroup.addCommand(submitJUnitXMLCommand);
|
|
11
|
+
|
|
12
|
+
export default junitGroup;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
|
|
6
|
+
import { APIClient } from '../../lib/api-client.js';
|
|
7
|
+
import { Logger } from '../../lib/logger.js';
|
|
8
|
+
|
|
9
|
+
export async function handleSubmitJUnitXML(options, { client } = {}) {
|
|
10
|
+
const {
|
|
11
|
+
domain,
|
|
12
|
+
token,
|
|
13
|
+
file,
|
|
14
|
+
name,
|
|
15
|
+
milestoneId,
|
|
16
|
+
milestoneName,
|
|
17
|
+
preserveNames,
|
|
18
|
+
skipRootSuite,
|
|
19
|
+
testEnvironmentId,
|
|
20
|
+
automationType,
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
const log = new Logger();
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(file)) {
|
|
26
|
+
log.error(`File not found at path ${file}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apiClient = client || new APIClient({ domain });
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { data } = await log.spin(
|
|
34
|
+
'Submitting...',
|
|
35
|
+
() =>
|
|
36
|
+
apiClient.submitJUnitReport({
|
|
37
|
+
token,
|
|
38
|
+
filePath: file,
|
|
39
|
+
name,
|
|
40
|
+
automationType,
|
|
41
|
+
milestoneId,
|
|
42
|
+
milestoneName,
|
|
43
|
+
preserveNames,
|
|
44
|
+
skipRootSuite,
|
|
45
|
+
testEnvironmentId,
|
|
46
|
+
}),
|
|
47
|
+
{
|
|
48
|
+
successMessage: 'Report submitted.',
|
|
49
|
+
errorMessage: 'Failed to submit.',
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
log.line();
|
|
54
|
+
log.table([
|
|
55
|
+
['JUnit file', data.name],
|
|
56
|
+
['Test run code', data.test_run.code],
|
|
57
|
+
['Test run URL', data.test_run.links.show],
|
|
58
|
+
]);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error.response?.data?.message || error.message;
|
|
61
|
+
|
|
62
|
+
log.line();
|
|
63
|
+
log.error(message, error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Command to submit a JUnit XML file
|
|
69
|
+
export const submitJUnitXMLCommand = new Command('submit')
|
|
70
|
+
.name('submit')
|
|
71
|
+
.description('Submit a JUnit XML file to your TestMonitor instance.')
|
|
72
|
+
.helpOption('-h, --help', 'Display help for command.')
|
|
73
|
+
.requiredOption('-d, --domain <domain>', 'Your TestMonitor domain name (e.g., mydomain.testmonitor.com)')
|
|
74
|
+
.requiredOption('-t, --token <id>', 'Your JUnit token')
|
|
75
|
+
.requiredOption('-f, --file <path>', 'Path to your JUnit XML file')
|
|
76
|
+
.option('-a, --automation-type <type>', 'The automation test type (playwright, phpunit, selenium).')
|
|
77
|
+
.option('-m, --milestone-id <id>', 'Milestone ID for the new test run (required if not set in configuration))')
|
|
78
|
+
.option('-o, --milestone-name <name>', 'Find or create a milestone by name for the test run.')
|
|
79
|
+
.option('-n, --name <name>', 'Custom name for the test run (auto-generated if omitted).')
|
|
80
|
+
.option('-p, --preserve-names', 'Preserve test case names exactly as in the XML file.')
|
|
81
|
+
.option('-s, --skip-root-suite', 'Skip the root suite and start from nested suites.')
|
|
82
|
+
.option('-e, --test-environment-id <id>', 'Test environment ID for the new test run.')
|
|
83
|
+
.option('-v, --verbose', 'Enable verbose debug output.')
|
|
84
|
+
.action((options) => handleSubmitJUnitXML(options));
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
import { appVersion } from './lib/app-version.js';
|
|
6
|
+
import junitGroup from './commands/junit/index.js';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('testmonitor-cli')
|
|
12
|
+
.version(await appVersion(), '-v, --version', 'Display the current CLI version.')
|
|
13
|
+
.description('CLI tool for interacting with the TestMonitor API.')
|
|
14
|
+
.helpCommand(false)
|
|
15
|
+
.helpOption('-h, --help', 'Display help for command.');
|
|
16
|
+
|
|
17
|
+
program.on('option:verbose', function () {
|
|
18
|
+
process.env.VERBOSE = this.opts().verbose;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
program.addCommand(junitGroup);
|
|
22
|
+
|
|
23
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
|
|
7
|
+
import { appVersion } from './app-version.js';
|
|
8
|
+
|
|
9
|
+
const cliVersion = await appVersion();
|
|
10
|
+
const nodeVersion = `NodeJS/${process.nodeVersion}`;
|
|
11
|
+
const osVersion = `${os.platform()} ${os.release()}`;
|
|
12
|
+
const userAgent = `TestMonitorCLI/${cliVersion} (${nodeVersion}; ${osVersion}; +https://www.testmonitor.com/)`;
|
|
13
|
+
|
|
14
|
+
export class APIClient {
|
|
15
|
+
constructor({ domain, httpClient }) {
|
|
16
|
+
this.baseURL = `https://${domain}/api/v1`;
|
|
17
|
+
|
|
18
|
+
this.httpClient =
|
|
19
|
+
httpClient ||
|
|
20
|
+
axios.create({
|
|
21
|
+
baseURL: this.baseURL,
|
|
22
|
+
headers: {
|
|
23
|
+
'User-Agent': userAgent,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async get(path, config = {}) {
|
|
29
|
+
return this._request('get', path, null, config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async post(path, data = {}, config = {}) {
|
|
33
|
+
return this._request('post', path, data, config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async put(path, data = {}, config = {}) {
|
|
37
|
+
return this._request('put', path, data, config);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async delete(path, config = {}) {
|
|
41
|
+
return this._request('delete', path, null, config);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _request(method, path, data, config) {
|
|
45
|
+
const isFormData = data instanceof FormData;
|
|
46
|
+
const headers = {
|
|
47
|
+
...(isFormData ? data.getHeaders() : { 'Content-Type': 'application/json' }),
|
|
48
|
+
...(config.headers || {}),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await this.httpClient.request({
|
|
53
|
+
method,
|
|
54
|
+
url: path,
|
|
55
|
+
data,
|
|
56
|
+
headers,
|
|
57
|
+
...config,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return response.data;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err.response?.data?.message || err.message;
|
|
63
|
+
throw new Error(message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Submit a JUnit report using plain data and file path
|
|
69
|
+
*/
|
|
70
|
+
async submitJUnitReport({
|
|
71
|
+
token,
|
|
72
|
+
filePath,
|
|
73
|
+
name,
|
|
74
|
+
automationType,
|
|
75
|
+
milestoneId,
|
|
76
|
+
milestoneName,
|
|
77
|
+
preserveNames,
|
|
78
|
+
skipRootSuite,
|
|
79
|
+
testEnvironmentId,
|
|
80
|
+
}) {
|
|
81
|
+
const form = new FormData();
|
|
82
|
+
|
|
83
|
+
form.append('token', token);
|
|
84
|
+
form.append('report', fs.createReadStream(filePath));
|
|
85
|
+
|
|
86
|
+
const optional = {
|
|
87
|
+
name,
|
|
88
|
+
milestone: milestoneName,
|
|
89
|
+
milestone_id: milestoneId,
|
|
90
|
+
preserve_names: preserveNames ? 1 : null,
|
|
91
|
+
skip_root_suite: skipRootSuite ? 1 : null,
|
|
92
|
+
test_environment_id: testEnvironmentId,
|
|
93
|
+
type: automationType,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const [key, value] of Object.entries(optional)) {
|
|
97
|
+
if (value !== undefined && value !== null) {
|
|
98
|
+
form.append(key, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.post('/reporters/junit/submit', form);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { table } from 'table';
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
#prefix = 'TestMonitor';
|
|
7
|
+
|
|
8
|
+
info(message) {
|
|
9
|
+
console.log(`${this.#prefix}: ${message}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
success(message) {
|
|
13
|
+
console.log(`${this.#prefix}: ${chalk.green(message)}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
warn(message) {
|
|
17
|
+
console.warn(`${this.#prefix}: ${chalk.yellow(message)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
error(message, err) {
|
|
21
|
+
const output = `${this.#prefix}: ${chalk.red(message)}`;
|
|
22
|
+
console.error(output);
|
|
23
|
+
|
|
24
|
+
if (process.env.DEBUG && err instanceof Error) {
|
|
25
|
+
console.error(chalk.gray(err.stack || err.message));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
debug(message) {
|
|
30
|
+
if (process.env.DEBUG) {
|
|
31
|
+
console.log(`${this.#prefix}: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
line(message = '') {
|
|
36
|
+
console.log(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async spin(message, callback, options = {}) {
|
|
40
|
+
const spinner = ora({
|
|
41
|
+
text: `${this.#prefix}: ${message}`,
|
|
42
|
+
color: 'green',
|
|
43
|
+
}).start();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await callback();
|
|
47
|
+
spinner.succeed(`${this.#prefix}: ${chalk.green(options.successMessage || message)}`);
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
spinner.fail(`${this.#prefix}: ${chalk.red(options.errorMessage || message)}`);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
table(rows, config) {
|
|
56
|
+
const output = table(rows, { ...{ singleLine: true }, ...config });
|
|
57
|
+
console.log(output);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/actions/SubmitReport.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import axios from 'axios';
|
|
3
|
-
import FormData from 'form-data';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { table, getBorderCharacters } from 'table';
|
|
6
|
-
|
|
7
|
-
export class SubmitReport {
|
|
8
|
-
constructor({ domain, apiKey, integrationId, file, name, type, milestoneId, milestoneName, preserveNames, skipRootSuite, testEnvironmentId }) {
|
|
9
|
-
this.domain = domain;
|
|
10
|
-
this.apiKey = apiKey;
|
|
11
|
-
this.integrationId = integrationId;
|
|
12
|
-
this.file = file;
|
|
13
|
-
this.name = name;
|
|
14
|
-
this.type = type;
|
|
15
|
-
this.milestoneId = milestoneId;
|
|
16
|
-
this.milestoneName = milestoneName;
|
|
17
|
-
this.preserveNames = preserveNames;
|
|
18
|
-
this.skipRootSuite = skipRootSuite;
|
|
19
|
-
this.testEnvironmentId = testEnvironmentId;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async run() {
|
|
23
|
-
const formData = new FormData();
|
|
24
|
-
|
|
25
|
-
// Required
|
|
26
|
-
formData.append('integration_id', this.integrationId);
|
|
27
|
-
formData.append('report', fs.createReadStream(this.file));
|
|
28
|
-
|
|
29
|
-
// Optional
|
|
30
|
-
const optional = {
|
|
31
|
-
name: this.name,
|
|
32
|
-
type: this.type,
|
|
33
|
-
milestone: this.milestoneName,
|
|
34
|
-
milestone_id: this.milestoneId,
|
|
35
|
-
preserve_names: this.preserveNames ? 1: null,
|
|
36
|
-
skip_root_suite: this.skipRootSuite ? 1 : null,
|
|
37
|
-
test_environment_id: this.testEnvironmentId,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
for (const [key, value] of Object.entries(optional)) {
|
|
41
|
-
if (value !== undefined && value !== null) {
|
|
42
|
-
formData.append(key, value);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const response = await axios.post(
|
|
48
|
-
`https://${this.domain}/api/v1/reporters/junit/submit`,
|
|
49
|
-
formData,
|
|
50
|
-
{
|
|
51
|
-
headers: {
|
|
52
|
-
...formData.getHeaders(),
|
|
53
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const { data } = response.data;
|
|
59
|
-
|
|
60
|
-
const config = {
|
|
61
|
-
border: getBorderCharacters('ramac'),
|
|
62
|
-
singleLine: true,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const rows = [
|
|
66
|
-
[chalk.bold('File'), data.name],
|
|
67
|
-
[chalk.bold('Date'), data.created_at],
|
|
68
|
-
[chalk.bold('ID'), data.test_run.id],
|
|
69
|
-
[chalk.bold('Code'), data.test_run.code],
|
|
70
|
-
[chalk.bold('Name'), data.test_run.name],
|
|
71
|
-
[chalk.bold('URL'), data.test_run.links.show],
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
console.log(table(rows, config));
|
|
75
|
-
|
|
76
|
-
console.log('Report submitted ' + chalk.green('successfully') + '.');
|
|
77
|
-
} catch (error) {
|
|
78
|
-
const message = error.response?.data?.message || error.message;
|
|
79
|
-
console.error(chalk.red('Error') + ': ' + message);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
package/commands/submit.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { SubmitReport } from '../actions/SubmitReport.js';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import 'dotenv/config';
|
|
6
|
-
|
|
7
|
-
const submitCommand = new Command('submit')
|
|
8
|
-
.description('Submit a JUnit XML file to your TestMonitor instance.')
|
|
9
|
-
.helpOption('-h, --help', 'Display help for command')
|
|
10
|
-
.requiredOption('-d, --domain <domain>', 'Your TestMonitor domain name (e.g., mydomain.testmonitor.com)')
|
|
11
|
-
.requiredOption('-i, --integration-id <id>', 'The JUnit integration ID')
|
|
12
|
-
.requiredOption('-f, --file <path>', 'Path to your JUnit XML file')
|
|
13
|
-
.option('-m, --milestone-id <id>', 'The milestone ID for the new test run (required if not configured in your JUnit configuration)')
|
|
14
|
-
.option('-o, --milestone-name <name>', 'Finds or creates a milestone matching the name for the new test run')
|
|
15
|
-
.option('-n, --name <name>', 'The name for the new test run (will be auto-generated if not provided)')
|
|
16
|
-
.option('-p, --preserve-names', 'Preserve test case names exactly as they appear in the XML file.')
|
|
17
|
-
.option('-s, --skip-root-suite', 'Skip the root test suite and start from its nested suites.')
|
|
18
|
-
.option('-e, --test-environment-id <id>', 'The test environment ID for the new test run')
|
|
19
|
-
.option('-t, --type <type>', 'The automation test type (cypress, playwright, phpunit, selenium)')
|
|
20
|
-
.option('-v, --verbose', 'Output additional debug information')
|
|
21
|
-
.action(async (options) => {
|
|
22
|
-
const {
|
|
23
|
-
domain,
|
|
24
|
-
integrationId,
|
|
25
|
-
file,
|
|
26
|
-
name,
|
|
27
|
-
milestoneId,
|
|
28
|
-
milestoneName,
|
|
29
|
-
preserveNames,
|
|
30
|
-
skipRootSuite,
|
|
31
|
-
testEnvironmentId,
|
|
32
|
-
type,
|
|
33
|
-
verbose
|
|
34
|
-
} = options;
|
|
35
|
-
|
|
36
|
-
const apiKey = process.env.TESTMONITOR_TOKEN;
|
|
37
|
-
|
|
38
|
-
if (!apiKey) {
|
|
39
|
-
console.error(chalk.red('Error') + ': TESTMONITOR_TOKEN environment variable is not set.');
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!fs.existsSync(file)) {
|
|
44
|
-
console.error(chalk.red('Error') + `: File not found at path ${file}`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const action = new SubmitReport({
|
|
49
|
-
domain,
|
|
50
|
-
apiKey,
|
|
51
|
-
integrationId,
|
|
52
|
-
file,
|
|
53
|
-
name,
|
|
54
|
-
type,
|
|
55
|
-
milestoneId,
|
|
56
|
-
milestoneName,
|
|
57
|
-
preserveNames,
|
|
58
|
-
skipRootSuite,
|
|
59
|
-
testEnvironmentId
|
|
60
|
-
});
|
|
61
|
-
await action.run();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
export default submitCommand;
|
package/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import submitCommand from './commands/submit.js';
|
|
5
|
-
import 'dotenv/config';
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
|
|
9
|
-
program
|
|
10
|
-
.name('testmonitor-cli')
|
|
11
|
-
.version('0.0.1')
|
|
12
|
-
.description('CLI tool for interacting with the TestMonitor API.');
|
|
13
|
-
|
|
14
|
-
program.addCommand(submitCommand);
|
|
15
|
-
|
|
16
|
-
program.on('option:verbose', function () {
|
|
17
|
-
process.env.VERBOSE = this.opts().verbose;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
program.parse(process.argv);
|