@testream/jest-reporter 0.1.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/LICENSE +30 -0
- package/README.md +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +13 -0
- package/dist/reporter.d.ts +39 -0
- package/dist/reporter.js +183 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +2 -0
- package/dist/uploader.d.ts +45 -0
- package/dist/uploader.js +237 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Testream Team. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are licensed,
|
|
6
|
+
not sold, to you for use only under the terms of this license.
|
|
7
|
+
|
|
8
|
+
GRANT OF LICENSE:
|
|
9
|
+
You are granted a non-exclusive, non-transferable license to use the Software
|
|
10
|
+
solely for the purpose of integrating test result reporting with the Testream service.
|
|
11
|
+
|
|
12
|
+
RESTRICTIONS:
|
|
13
|
+
- You may not modify, adapt, or create derivative works of the Software
|
|
14
|
+
- You may not distribute, sublicense, lease, or rent the Software
|
|
15
|
+
- You may not reverse engineer, decompile, or disassemble the Software
|
|
16
|
+
- The Software may only be used in conjunction with the Testream service
|
|
17
|
+
|
|
18
|
+
OWNERSHIP:
|
|
19
|
+
The Software is licensed, not sold. This license does not grant you any rights
|
|
20
|
+
to trademarks or service marks.
|
|
21
|
+
|
|
22
|
+
TERMINATION:
|
|
23
|
+
This license is effective until terminated. Your rights under this license will
|
|
24
|
+
terminate automatically without notice if you fail to comply with any term of
|
|
25
|
+
this license.
|
|
26
|
+
|
|
27
|
+
DISCLAIMER:
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @testream/jest-reporter
|
|
2
|
+
|
|
3
|
+
Jest reporter that generates CTRF test reports and uploads them to Testream.
|
|
4
|
+
|
|
5
|
+
- **Docs:** https://testream.github.io/docs/reporters/jest
|
|
6
|
+
- **Docs repository:** https://github.com/testream/docs
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
Install the reporter:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --save-dev @testream/jest-reporter
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Configure in your `jest.config.js`:
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
module.exports = {
|
|
20
|
+
reporters: [
|
|
21
|
+
'default',
|
|
22
|
+
[
|
|
23
|
+
'@testream/jest-reporter',
|
|
24
|
+
{
|
|
25
|
+
apiKey: process.env.TESTREAM_API_KEY,
|
|
26
|
+
projectKey: 'PROJ',
|
|
27
|
+
uploadEnabled: true,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run your tests:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The reporter will automatically:
|
|
41
|
+
1. Generate a CTRF report in `ctrf/ctrf-report.json`
|
|
42
|
+
2. Upload test results to Testream
|
|
43
|
+
3. Display results in Jira
|
|
44
|
+
|
|
45
|
+
## Configuration Options
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
{
|
|
49
|
+
// Required
|
|
50
|
+
apiKey: string; // Testream API key
|
|
51
|
+
projectKey: string; // Jira project key
|
|
52
|
+
|
|
53
|
+
// Optional - Git context (auto-detected in CI)
|
|
54
|
+
branch?: string; // Git branch name
|
|
55
|
+
commitSha?: string; // Git commit SHA
|
|
56
|
+
repositoryUrl?: string; // Git repository URL
|
|
57
|
+
|
|
58
|
+
// Optional - Build metadata (auto-detected in CI)
|
|
59
|
+
buildName?: string; // Build name/identifier
|
|
60
|
+
buildNumber?: string; // Build number
|
|
61
|
+
buildUrl?: string; // CI pipeline URL
|
|
62
|
+
|
|
63
|
+
// Optional - Environment metadata
|
|
64
|
+
testEnvironment?: string; // e.g., 'staging', 'production', 'ci'
|
|
65
|
+
appName?: string; // Application name under test
|
|
66
|
+
appVersion?: string; // Application version
|
|
67
|
+
testType?: string; // e.g., 'unit', 'integration', 'e2e'
|
|
68
|
+
|
|
69
|
+
// Optional - Upload options
|
|
70
|
+
uploadEnabled?: boolean; // Enable/disable upload (default: true)
|
|
71
|
+
failOnUploadError?: boolean; // Fail tests if upload fails (default: false)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Basic Usage
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
// jest.config.js
|
|
81
|
+
module.exports = {
|
|
82
|
+
reporters: [
|
|
83
|
+
'default',
|
|
84
|
+
[
|
|
85
|
+
'@testream/jest-reporter',
|
|
86
|
+
{
|
|
87
|
+
apiKey: process.env.TESTREAM_API_KEY,
|
|
88
|
+
projectKey: 'MYPROJ',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### With Environment Metadata
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// jest.config.js
|
|
99
|
+
module.exports = {
|
|
100
|
+
reporters: [
|
|
101
|
+
'default',
|
|
102
|
+
[
|
|
103
|
+
'@testream/jest-reporter',
|
|
104
|
+
{
|
|
105
|
+
apiKey: process.env.TESTREAM_API_KEY,
|
|
106
|
+
projectKey: 'MYPROJ',
|
|
107
|
+
testEnvironment: 'ci',
|
|
108
|
+
appName: 'MyApp',
|
|
109
|
+
appVersion: '1.0.0',
|
|
110
|
+
testType: 'unit',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Generate CTRF Only (No Upload)
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// jest.config.js
|
|
121
|
+
module.exports = {
|
|
122
|
+
reporters: [
|
|
123
|
+
'default',
|
|
124
|
+
[
|
|
125
|
+
'@testream/jest-reporter',
|
|
126
|
+
{
|
|
127
|
+
apiKey: '', // Not needed when upload is disabled
|
|
128
|
+
projectKey: '',
|
|
129
|
+
uploadEnabled: false,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## How It Works
|
|
137
|
+
|
|
138
|
+
1. The reporter uses `jest-ctrf-json-reporter` internally to generate CTRF reports
|
|
139
|
+
2. Hooks into Jest's test lifecycle to collect results
|
|
140
|
+
3. Converts Jest results to CTRF (Common Test Report Format)
|
|
141
|
+
4. Writes CTRF report to `ctrf/ctrf-report.json`
|
|
142
|
+
5. Optionally uploads results to Testream backend
|
|
143
|
+
6. Test results appear in Jira automatically
|
|
144
|
+
|
|
145
|
+
## CI/CD Integration
|
|
146
|
+
|
|
147
|
+
The reporter auto-detects CI context from:
|
|
148
|
+
- GitHub Actions
|
|
149
|
+
- GitLab CI
|
|
150
|
+
- Azure Pipelines
|
|
151
|
+
- CircleCI
|
|
152
|
+
- Jenkins
|
|
153
|
+
- Bitbucket Pipelines
|
|
154
|
+
|
|
155
|
+
You can also manually specify git/build context via configuration options.
|
|
156
|
+
|
|
157
|
+
For full documentation and examples, see https://testream.github.io/docs/reporters/jest.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @testream/jest-reporter
|
|
3
|
+
*
|
|
4
|
+
* Jest reporter that generates CTRF-format test reports and
|
|
5
|
+
* automatically uploads them to the Testream backend.
|
|
6
|
+
*/
|
|
7
|
+
export { TestreamJestReporter as default } from './reporter';
|
|
8
|
+
export { TestreamJestReporter } from './reporter';
|
|
9
|
+
export type { JestReporterConfig } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @testream/jest-reporter
|
|
4
|
+
*
|
|
5
|
+
* Jest reporter that generates CTRF-format test reports and
|
|
6
|
+
* automatically uploads them to the Testream backend.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TestreamJestReporter = exports.default = void 0;
|
|
10
|
+
var reporter_1 = require("./reporter");
|
|
11
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return reporter_1.TestreamJestReporter; } });
|
|
12
|
+
var reporter_2 = require("./reporter");
|
|
13
|
+
Object.defineProperty(exports, "TestreamJestReporter", { enumerable: true, get: function () { return reporter_2.TestreamJestReporter; } });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Reporter, Test, TestContext } from '@jest/reporters';
|
|
2
|
+
import type { AggregatedResult, TestResult } from '@jest/test-result';
|
|
3
|
+
import { JestReporterConfig } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Testream Jest Reporter
|
|
6
|
+
* Wraps jest-ctrf-json-reporter to generate CTRF reports and upload them to Testream
|
|
7
|
+
*/
|
|
8
|
+
export declare class TestreamJestReporter implements Reporter {
|
|
9
|
+
private config;
|
|
10
|
+
private ctrfReporter;
|
|
11
|
+
private readonly outputDir;
|
|
12
|
+
private readonly outputFile;
|
|
13
|
+
constructor(globalConfig: unknown, options?: JestReporterConfig, context?: unknown);
|
|
14
|
+
/**
|
|
15
|
+
* Called when test run starts
|
|
16
|
+
*/
|
|
17
|
+
onRunStart(_aggregatedResult: AggregatedResult, _options: unknown): void | Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Called when an individual test starts
|
|
20
|
+
*/
|
|
21
|
+
onTestStart(_test: Test): void | Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Called when a test file completes
|
|
24
|
+
*/
|
|
25
|
+
onTestResult(test: Test, testResult: TestResult, _aggregatedResult: AggregatedResult): void | Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Called when entire test run completes
|
|
28
|
+
*/
|
|
29
|
+
onRunComplete(_testContexts: Set<TestContext>, _results: AggregatedResult): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Read CTRF report from file
|
|
32
|
+
*/
|
|
33
|
+
private readCTRFReport;
|
|
34
|
+
/**
|
|
35
|
+
* Upload report to Testream API
|
|
36
|
+
*/
|
|
37
|
+
private uploadReport;
|
|
38
|
+
}
|
|
39
|
+
export default TestreamJestReporter;
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.TestreamJestReporter = void 0;
|
|
40
|
+
const jest_ctrf_json_reporter_1 = __importDefault(require("jest-ctrf-json-reporter"));
|
|
41
|
+
const fs = __importStar(require("fs/promises"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const uploader_1 = require("./uploader");
|
|
44
|
+
/**
|
|
45
|
+
* Testream Jest Reporter
|
|
46
|
+
* Wraps jest-ctrf-json-reporter to generate CTRF reports and upload them to Testream
|
|
47
|
+
*/
|
|
48
|
+
class TestreamJestReporter {
|
|
49
|
+
constructor(globalConfig, options = {
|
|
50
|
+
apiKey: '',
|
|
51
|
+
projectKey: '',
|
|
52
|
+
uploadEnabled: true,
|
|
53
|
+
}, context = {}) {
|
|
54
|
+
this.outputDir = 'ctrf';
|
|
55
|
+
this.outputFile = 'ctrf-report.json';
|
|
56
|
+
this.config = {
|
|
57
|
+
uploadEnabled: true,
|
|
58
|
+
failOnUploadError: false,
|
|
59
|
+
...options,
|
|
60
|
+
};
|
|
61
|
+
// Initialize jest-ctrf-json-reporter with fixed output path and metadata
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
const ctrfConfig = {
|
|
64
|
+
outputDir: this.outputDir,
|
|
65
|
+
outputFile: this.outputFile,
|
|
66
|
+
};
|
|
67
|
+
// Add environment metadata (same pattern as playwright-reporter)
|
|
68
|
+
if (this.config.testEnvironment)
|
|
69
|
+
ctrfConfig.testEnvironment = this.config.testEnvironment;
|
|
70
|
+
if (this.config.appName)
|
|
71
|
+
ctrfConfig.appName = this.config.appName;
|
|
72
|
+
if (this.config.appVersion)
|
|
73
|
+
ctrfConfig.appVersion = this.config.appVersion;
|
|
74
|
+
if (this.config.buildName)
|
|
75
|
+
ctrfConfig.buildName = this.config.buildName;
|
|
76
|
+
if (this.config.buildNumber)
|
|
77
|
+
ctrfConfig.buildNumber = this.config.buildNumber;
|
|
78
|
+
if (this.config.buildUrl)
|
|
79
|
+
ctrfConfig.buildUrl = this.config.buildUrl;
|
|
80
|
+
if (this.config.testType)
|
|
81
|
+
ctrfConfig.testType = this.config.testType;
|
|
82
|
+
if (this.config.branch)
|
|
83
|
+
ctrfConfig.branchName = this.config.branch;
|
|
84
|
+
if (this.config.commitSha)
|
|
85
|
+
ctrfConfig.commit = this.config.commitSha;
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
this.ctrfReporter = new jest_ctrf_json_reporter_1.default(globalConfig, ctrfConfig, context);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Called when test run starts
|
|
91
|
+
*/
|
|
92
|
+
onRunStart(_aggregatedResult, _options) {
|
|
93
|
+
if (this.ctrfReporter.onRunStart) {
|
|
94
|
+
return this.ctrfReporter.onRunStart();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Called when an individual test starts
|
|
99
|
+
*/
|
|
100
|
+
onTestStart(_test) {
|
|
101
|
+
if (this.ctrfReporter.onTestStart) {
|
|
102
|
+
return this.ctrfReporter.onTestStart();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Called when a test file completes
|
|
107
|
+
*/
|
|
108
|
+
onTestResult(test, testResult, _aggregatedResult) {
|
|
109
|
+
if (this.ctrfReporter.onTestResult) {
|
|
110
|
+
return this.ctrfReporter.onTestResult(test, testResult);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Called when entire test run completes
|
|
115
|
+
*/
|
|
116
|
+
async onRunComplete(_testContexts, _results) {
|
|
117
|
+
// Let jest-ctrf-json-reporter complete and write the CTRF file
|
|
118
|
+
if (this.ctrfReporter.onRunComplete) {
|
|
119
|
+
await this.ctrfReporter.onRunComplete();
|
|
120
|
+
}
|
|
121
|
+
// Read the generated CTRF report
|
|
122
|
+
const report = await this.readCTRFReport();
|
|
123
|
+
if (!report) {
|
|
124
|
+
uploader_1.defaultLogger.error('Failed to read CTRF report, skipping upload');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
uploader_1.defaultLogger.info(`CTRF report written to: ${path.join(this.outputDir, this.outputFile)}`);
|
|
128
|
+
// Upload to API if enabled
|
|
129
|
+
if (this.config.uploadEnabled && this.config.apiKey && this.config.projectKey) {
|
|
130
|
+
await this.uploadReport(report);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read CTRF report from file
|
|
135
|
+
*/
|
|
136
|
+
async readCTRFReport() {
|
|
137
|
+
const reportPath = path.join(this.outputDir, this.outputFile);
|
|
138
|
+
try {
|
|
139
|
+
const content = await fs.readFile(reportPath, 'utf-8');
|
|
140
|
+
return JSON.parse(content);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
uploader_1.defaultLogger.error(`Failed to read CTRF report from ${reportPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Upload report to Testream API
|
|
149
|
+
*/
|
|
150
|
+
async uploadReport(report) {
|
|
151
|
+
const ciContext = (0, uploader_1.detectCIContext)();
|
|
152
|
+
// Merge CLI options with auto-detected CI context (config takes precedence)
|
|
153
|
+
const branch = this.config.branch || ciContext.branch;
|
|
154
|
+
const commitSha = this.config.commitSha || ciContext.commitSha;
|
|
155
|
+
const repositoryUrl = this.config.repositoryUrl || ciContext.repositoryUrl;
|
|
156
|
+
const buildNumber = this.config.buildNumber || ciContext.buildNumber;
|
|
157
|
+
const buildUrl = this.config.buildUrl || ciContext.buildUrl;
|
|
158
|
+
const result = await (0, uploader_1.uploadToApi)({
|
|
159
|
+
report,
|
|
160
|
+
projectKey: this.config.projectKey,
|
|
161
|
+
apiKey: this.config.apiKey,
|
|
162
|
+
// Git context
|
|
163
|
+
branch,
|
|
164
|
+
commitSha,
|
|
165
|
+
repositoryUrl,
|
|
166
|
+
// Build metadata
|
|
167
|
+
buildName: this.config.buildName,
|
|
168
|
+
buildNumber,
|
|
169
|
+
buildUrl,
|
|
170
|
+
// Environment metadata
|
|
171
|
+
testEnvironment: this.config.testEnvironment,
|
|
172
|
+
appName: this.config.appName,
|
|
173
|
+
appVersion: this.config.appVersion,
|
|
174
|
+
testType: this.config.testType,
|
|
175
|
+
logger: uploader_1.defaultLogger,
|
|
176
|
+
});
|
|
177
|
+
if (!result.success && this.config.failOnUploadError) {
|
|
178
|
+
throw new Error(`Upload failed: ${result.error}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.TestreamJestReporter = TestreamJestReporter;
|
|
183
|
+
exports.default = TestreamJestReporter;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export CTRF types from shared-types package
|
|
3
|
+
*/
|
|
4
|
+
export type { CTRFReport, CTRFTest, CTRFSummary, CTRFResults, CTRFAttachment, CTRFStep, } from '@jira-test-manager/shared-types';
|
|
5
|
+
/**
|
|
6
|
+
* Re-export API types from shared-types package
|
|
7
|
+
*/
|
|
8
|
+
export type { IngestResponse, ApiErrorResponse, UploadResult, } from '@jira-test-manager/shared-types';
|
|
9
|
+
/**
|
|
10
|
+
* Reporter Configuration Options
|
|
11
|
+
*/
|
|
12
|
+
export interface JestReporterConfig {
|
|
13
|
+
/**
|
|
14
|
+
* API key for authentication
|
|
15
|
+
* @required
|
|
16
|
+
*/
|
|
17
|
+
apiKey: string;
|
|
18
|
+
/**
|
|
19
|
+
* Project key to associate test results with
|
|
20
|
+
* @required
|
|
21
|
+
*/
|
|
22
|
+
projectKey: string;
|
|
23
|
+
/**
|
|
24
|
+
* Git branch name (auto-detected from CI environment if not provided)
|
|
25
|
+
*/
|
|
26
|
+
branch?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Git commit SHA (auto-detected from CI environment if not provided)
|
|
29
|
+
*/
|
|
30
|
+
commitSha?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Git repository URL (auto-detected from CI environment if not provided)
|
|
33
|
+
*/
|
|
34
|
+
repositoryUrl?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Enable/disable automatic upload to backend
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
uploadEnabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Fail the process if upload fails
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
failOnUploadError?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Test type identifier (e.g., 'unit', 'integration', 'e2e')
|
|
47
|
+
*/
|
|
48
|
+
testType?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Application name under test
|
|
51
|
+
*/
|
|
52
|
+
appName?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Application version under test
|
|
55
|
+
*/
|
|
56
|
+
appVersion?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Build name/identifier
|
|
59
|
+
*/
|
|
60
|
+
buildName?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Build number
|
|
63
|
+
*/
|
|
64
|
+
buildNumber?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Build URL (CI pipeline URL)
|
|
67
|
+
*/
|
|
68
|
+
buildUrl?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Test environment (e.g., 'staging', 'production', 'ci')
|
|
71
|
+
*/
|
|
72
|
+
testEnvironment?: string;
|
|
73
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CTRFReport, UploadResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Logger interface for output
|
|
4
|
+
*/
|
|
5
|
+
export interface Logger {
|
|
6
|
+
info(message: string): void;
|
|
7
|
+
warning(message: string): void;
|
|
8
|
+
error(message: string): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Default console logger
|
|
12
|
+
*/
|
|
13
|
+
export declare const defaultLogger: Logger;
|
|
14
|
+
export interface UploadToApiOptions {
|
|
15
|
+
report: CTRFReport;
|
|
16
|
+
projectKey: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
commitSha?: string;
|
|
19
|
+
branch?: string;
|
|
20
|
+
repositoryUrl?: string;
|
|
21
|
+
buildName?: string;
|
|
22
|
+
buildNumber?: string;
|
|
23
|
+
buildUrl?: string;
|
|
24
|
+
testEnvironment?: string;
|
|
25
|
+
appName?: string;
|
|
26
|
+
appVersion?: string;
|
|
27
|
+
testType?: string;
|
|
28
|
+
logger?: Logger;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Upload CTRF report to Testream API
|
|
32
|
+
*/
|
|
33
|
+
export declare function uploadToApi(options: UploadToApiOptions): Promise<UploadResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Detect CI environment and extract context
|
|
36
|
+
*/
|
|
37
|
+
export interface CIContext {
|
|
38
|
+
branch?: string;
|
|
39
|
+
commitSha?: string;
|
|
40
|
+
repositoryUrl?: string;
|
|
41
|
+
buildNumber?: string;
|
|
42
|
+
buildUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function detectCIContext(): CIContext;
|
|
45
|
+
export declare const detectGitContext: typeof detectCIContext;
|
package/dist/uploader.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectGitContext = exports.defaultLogger = void 0;
|
|
4
|
+
exports.uploadToApi = uploadToApi;
|
|
5
|
+
exports.detectCIContext = detectCIContext;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
/**
|
|
8
|
+
* Hardcoded API URL for Testream backend
|
|
9
|
+
*/
|
|
10
|
+
const API_URL = 'https://test-manager-backend.fly.dev';
|
|
11
|
+
/**
|
|
12
|
+
* Default console logger
|
|
13
|
+
*/
|
|
14
|
+
exports.defaultLogger = {
|
|
15
|
+
info: (msg) => console.log(msg),
|
|
16
|
+
warning: (msg) => console.warn(msg),
|
|
17
|
+
error: (msg) => console.error(msg),
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Upload CTRF report to Testream API
|
|
21
|
+
*/
|
|
22
|
+
async function uploadToApi(options) {
|
|
23
|
+
const { report, projectKey, apiKey, commitSha = '', branch = '', repositoryUrl = '', buildName, buildNumber, buildUrl, testEnvironment, appName, appVersion, testType, logger = exports.defaultLogger, } = options;
|
|
24
|
+
// Ensure reportId exists
|
|
25
|
+
if (!report.reportId) {
|
|
26
|
+
report.reportId = (0, crypto_1.randomUUID)();
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
logger.info('='.repeat(60));
|
|
30
|
+
logger.info('Testream - Jest Test Results Upload');
|
|
31
|
+
logger.info('='.repeat(60));
|
|
32
|
+
logger.info(`Test Summary:`);
|
|
33
|
+
logger.info(` Total: ${report.results.summary.tests}`);
|
|
34
|
+
logger.info(` Passed: ${report.results.summary.passed}`);
|
|
35
|
+
logger.info(` Failed: ${report.results.summary.failed}`);
|
|
36
|
+
logger.info(` Skipped: ${report.results.summary.skipped}`);
|
|
37
|
+
logger.info('');
|
|
38
|
+
// Log git context if provided
|
|
39
|
+
if (branch || commitSha || repositoryUrl) {
|
|
40
|
+
logger.info(`Git Context:`);
|
|
41
|
+
if (branch)
|
|
42
|
+
logger.info(` Branch: ${branch}`);
|
|
43
|
+
if (commitSha)
|
|
44
|
+
logger.info(` Commit: ${commitSha.substring(0, 7)}`);
|
|
45
|
+
if (repositoryUrl)
|
|
46
|
+
logger.info(` Repository: ${repositoryUrl}`);
|
|
47
|
+
logger.info('');
|
|
48
|
+
}
|
|
49
|
+
// Log build info if provided
|
|
50
|
+
if (buildName || buildNumber || buildUrl) {
|
|
51
|
+
logger.info(`Build Info:`);
|
|
52
|
+
if (buildName)
|
|
53
|
+
logger.info(` Name: ${buildName}`);
|
|
54
|
+
if (buildNumber)
|
|
55
|
+
logger.info(` Number: ${buildNumber}`);
|
|
56
|
+
if (buildUrl)
|
|
57
|
+
logger.info(` URL: ${buildUrl}`);
|
|
58
|
+
logger.info('');
|
|
59
|
+
}
|
|
60
|
+
// Log environment info if provided
|
|
61
|
+
if (testEnvironment || appName || appVersion || testType) {
|
|
62
|
+
logger.info(`Environment:`);
|
|
63
|
+
if (testEnvironment)
|
|
64
|
+
logger.info(` Environment: ${testEnvironment}`);
|
|
65
|
+
if (appName)
|
|
66
|
+
logger.info(` App: ${appName}`);
|
|
67
|
+
if (appVersion)
|
|
68
|
+
logger.info(` Version: ${appVersion}`);
|
|
69
|
+
if (testType)
|
|
70
|
+
logger.info(` Test Type: ${testType}`);
|
|
71
|
+
logger.info('');
|
|
72
|
+
}
|
|
73
|
+
// Prepare payload with all metadata
|
|
74
|
+
const ingestPayload = {
|
|
75
|
+
report,
|
|
76
|
+
reportId: report.reportId,
|
|
77
|
+
projectKey,
|
|
78
|
+
// Git context
|
|
79
|
+
commitSha,
|
|
80
|
+
branch,
|
|
81
|
+
repositoryUrl,
|
|
82
|
+
// Build metadata
|
|
83
|
+
buildName,
|
|
84
|
+
buildNumber,
|
|
85
|
+
buildUrl,
|
|
86
|
+
// Environment metadata
|
|
87
|
+
testEnvironment,
|
|
88
|
+
appName,
|
|
89
|
+
appVersion,
|
|
90
|
+
testType,
|
|
91
|
+
};
|
|
92
|
+
// Upload to API
|
|
93
|
+
logger.info(`Uploading test results...`);
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await fetch(`${API_URL}/api/v1/ingest`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'X-API-KEY': apiKey,
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(ingestPayload),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
107
|
+
throw new Error(`Failed to connect to API: ${errorMessage}`);
|
|
108
|
+
}
|
|
109
|
+
// Handle errors
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const responseText = await response.text();
|
|
112
|
+
let errorData = null;
|
|
113
|
+
try {
|
|
114
|
+
errorData = JSON.parse(responseText);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Not JSON
|
|
118
|
+
}
|
|
119
|
+
// Handle 409 (already exists)
|
|
120
|
+
if (response.status === 409) {
|
|
121
|
+
logger.warning(`Report already exists: ${errorData?.reportId || 'unknown'}`);
|
|
122
|
+
logger.info('This is expected if the workflow was re-run');
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
reportId: errorData?.reportId || report.reportId,
|
|
126
|
+
summary: {
|
|
127
|
+
passed: report.results.summary.passed,
|
|
128
|
+
failed: report.results.summary.failed,
|
|
129
|
+
skipped: report.results.summary.skipped,
|
|
130
|
+
total: report.results.summary.tests,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Other errors
|
|
135
|
+
const errorMessage = errorData?.error
|
|
136
|
+
? `${errorData.error}${errorData.details ? `: ${errorData.details}` : ''}`
|
|
137
|
+
: responseText || `HTTP ${response.status}`;
|
|
138
|
+
throw new Error(`API ingest failed (${response.status}): ${errorMessage}`);
|
|
139
|
+
}
|
|
140
|
+
const result = (await response.json());
|
|
141
|
+
logger.info('');
|
|
142
|
+
logger.info('='.repeat(60));
|
|
143
|
+
logger.info('Upload completed successfully');
|
|
144
|
+
logger.info(` Report ID: ${result.reportId}`);
|
|
145
|
+
logger.info(` Test Run ID: ${result.testRunId}`);
|
|
146
|
+
logger.info('='.repeat(60));
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
reportId: result.reportId,
|
|
150
|
+
testRunId: result.testRunId,
|
|
151
|
+
summary: result.summary,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
156
|
+
logger.error(`Upload failed: ${errorMessage}`);
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
reportId: '',
|
|
160
|
+
error: errorMessage,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function detectCIContext() {
|
|
165
|
+
const env = process.env;
|
|
166
|
+
// GitHub Actions
|
|
167
|
+
if (env.GITHUB_ACTIONS === 'true') {
|
|
168
|
+
return {
|
|
169
|
+
branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || env.GITHUB_REF?.replace('refs/heads/', ''),
|
|
170
|
+
commitSha: env.GITHUB_SHA,
|
|
171
|
+
repositoryUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY
|
|
172
|
+
? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`
|
|
173
|
+
: undefined,
|
|
174
|
+
buildNumber: env.GITHUB_RUN_NUMBER,
|
|
175
|
+
buildUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID
|
|
176
|
+
? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`
|
|
177
|
+
: undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// GitLab CI
|
|
181
|
+
if (env.GITLAB_CI === 'true') {
|
|
182
|
+
return {
|
|
183
|
+
branch: env.CI_COMMIT_BRANCH || env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
184
|
+
commitSha: env.CI_COMMIT_SHA,
|
|
185
|
+
repositoryUrl: env.CI_PROJECT_URL,
|
|
186
|
+
buildNumber: env.CI_PIPELINE_IID,
|
|
187
|
+
buildUrl: env.CI_PIPELINE_URL,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Azure Pipelines
|
|
191
|
+
if (env.TF_BUILD === 'True') {
|
|
192
|
+
return {
|
|
193
|
+
branch: env.BUILD_SOURCEBRANCH?.replace('refs/heads/', ''),
|
|
194
|
+
commitSha: env.BUILD_SOURCEVERSION,
|
|
195
|
+
repositoryUrl: env.BUILD_REPOSITORY_URI,
|
|
196
|
+
buildNumber: env.BUILD_BUILDNUMBER,
|
|
197
|
+
buildUrl: env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI && env.SYSTEM_TEAMPROJECT && env.BUILD_BUILDID
|
|
198
|
+
? `${env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`
|
|
199
|
+
: undefined,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// CircleCI
|
|
203
|
+
if (env.CIRCLECI === 'true') {
|
|
204
|
+
return {
|
|
205
|
+
branch: env.CIRCLE_BRANCH,
|
|
206
|
+
commitSha: env.CIRCLE_SHA1,
|
|
207
|
+
repositoryUrl: env.CIRCLE_REPOSITORY_URL,
|
|
208
|
+
buildNumber: env.CIRCLE_BUILD_NUM,
|
|
209
|
+
buildUrl: env.CIRCLE_BUILD_URL,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Jenkins
|
|
213
|
+
if (env.JENKINS_URL) {
|
|
214
|
+
return {
|
|
215
|
+
branch: env.GIT_BRANCH?.replace('origin/', ''),
|
|
216
|
+
commitSha: env.GIT_COMMIT,
|
|
217
|
+
repositoryUrl: env.GIT_URL,
|
|
218
|
+
buildNumber: env.BUILD_NUMBER,
|
|
219
|
+
buildUrl: env.BUILD_URL,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
// Bitbucket Pipelines
|
|
223
|
+
if (env.BITBUCKET_BUILD_NUMBER) {
|
|
224
|
+
return {
|
|
225
|
+
branch: env.BITBUCKET_BRANCH,
|
|
226
|
+
commitSha: env.BITBUCKET_COMMIT,
|
|
227
|
+
repositoryUrl: env.BITBUCKET_GIT_HTTP_ORIGIN,
|
|
228
|
+
buildNumber: env.BITBUCKET_BUILD_NUMBER,
|
|
229
|
+
buildUrl: env.BITBUCKET_REPO_FULL_NAME && env.BITBUCKET_BUILD_NUMBER
|
|
230
|
+
? `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`
|
|
231
|
+
: undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
// Keep old function name for backwards compatibility
|
|
237
|
+
exports.detectGitContext = detectCIContext;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testream/jest-reporter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Jest CTRF reporter with automatic upload to Testream",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"format": "prettier --write '**/*.ts'",
|
|
10
|
+
"lint": "eslint src/**/*.ts",
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"test": "echo \"No tests yet\" && exit 0",
|
|
13
|
+
"all": "npm run format && npm run lint && npm run build"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/testream/docs.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"jest",
|
|
21
|
+
"reporter",
|
|
22
|
+
"testing",
|
|
23
|
+
"test-management",
|
|
24
|
+
"ctrf",
|
|
25
|
+
"test-results",
|
|
26
|
+
"jira"
|
|
27
|
+
],
|
|
28
|
+
"author": "Testream",
|
|
29
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@jira-test-manager/shared-types": "*",
|
|
35
|
+
"jest-ctrf-json-reporter": "^0.0.10"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@jest/reporters": ">=27.0.0",
|
|
39
|
+
"@jest/test-result": ">=27.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@jest/reporters": "^29.0.0",
|
|
43
|
+
"@jest/test-result": "^29.0.0",
|
|
44
|
+
"@types/node": "^20.10.6",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
|
46
|
+
"@typescript-eslint/parser": "^6.17.0",
|
|
47
|
+
"eslint": "^8.56.0",
|
|
48
|
+
"prettier": "^3.1.1",
|
|
49
|
+
"typescript": "^5.3.3"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
]
|
|
59
|
+
}
|