@testomatio/reporter 1.0.0 β 1.2.0-beta
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/README.md +9 -3
- package/lib/{ArtifactStorage.js β _ArtifactStorageOld.js} +15 -4
- package/lib/adapter/jest.js +15 -2
- package/lib/adapter/mocha.js +1 -1
- package/lib/artifactStorage.js +25 -0
- package/lib/client.js +16 -9
- package/lib/constants.js +5 -0
- package/lib/dataStorage.js +172 -0
- package/lib/logger.js +301 -0
- package/lib/reporter.js +8 -1
- package/lib/util.js +53 -33
- package/package.json +1 -1
- package/Changelog.md +0 -264
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ Testomat.io Reporter (this npm package) supports:
|
|
|
16
16
|
* π
Realtime reports
|
|
17
17
|
* ποΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
|
|
18
18
|
* πΆββοΈ Steps *(work in progress)*
|
|
19
|
-
*
|
|
19
|
+
* π Logger *(work in progress, supports Jest for now)*
|
|
20
|
+
* βοΈ Custom properties and metadata *(work in progress)*
|
|
20
21
|
* π― Free & open-source.
|
|
21
22
|
* π Public and private Run reports on cloud via [Testomat.io App](https://testomat.io) π
|
|
22
23
|
|
|
@@ -64,7 +65,7 @@ yarn add @testomatio/reporter --dev
|
|
|
64
65
|
|
|
65
66
|
### 1οΈβ£ Attach Reporter to the Test Runner
|
|
66
67
|
|
|
67
|
-
* ####
|
|
68
|
+
* #### [Playwright](./docs/frameworks.md#playwright)
|
|
68
69
|
* #### [CodeceptJS](./docs/frameworks.md#CodeceptJS)
|
|
69
70
|
* #### [Cypress](./docs/frameworks.md#Cypress)
|
|
70
71
|
* #### [Jest](./docs/frameworks.md#Jest)
|
|
@@ -99,7 +100,11 @@ GitHub report published as a comment to Pull Request:
|
|
|
99
100
|
1. Create bucket on AWS, Google Cloud, or any other cloud storage provider supporting S3 protocol.
|
|
100
101
|
2. [Pass S3 credentials](./docs/artifacts.md) to reporter to enable artifacts uploading.
|
|
101
102
|
|
|
102
|
-
### 4οΈβ£
|
|
103
|
+
### 4οΈβ£ Use Logger
|
|
104
|
+
|
|
105
|
+
Intercept your logger messages or log anything with our [Logger](./docs/logger.md) (_work in progress_).
|
|
106
|
+
|
|
107
|
+
### 5οΈβ£ Add to CI Pipeline
|
|
103
108
|
|
|
104
109
|
After you tested reporter locally add it to your CI pipeline.
|
|
105
110
|
|
|
@@ -119,6 +124,7 @@ Bring this reporter on CI and never lose test results again!
|
|
|
119
124
|
* π [JUnit](./docs/junit.md)
|
|
120
125
|
* ποΈ [Artifacts](./docs/artifacts.md)
|
|
121
126
|
* π [Workflows](./docs/workflows.md)
|
|
127
|
+
* π [Logger](./docs/logger.md)
|
|
122
128
|
|
|
123
129
|
## Development
|
|
124
130
|
|
|
@@ -7,24 +7,29 @@ const { TESTOMAT_ARTIFACT_SUFFIX } = require('./constants');
|
|
|
7
7
|
const { specificTestInfo } = require('./util');
|
|
8
8
|
|
|
9
9
|
class ArtifactStorage {
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
constructor(params) {
|
|
12
|
-
this._tmpPrefix = "tsmt_reporter";
|
|
13
12
|
this.isFile = false;
|
|
14
13
|
this.isMemory = true;
|
|
15
14
|
this.storage = params?.toFile;
|
|
16
15
|
|
|
17
16
|
if (this.storage) {
|
|
17
|
+
// this is fine to use when you start saving artifacts;
|
|
18
|
+
// but when you need to retrieve them, you will not know if there is "isFile" param,
|
|
19
|
+
// because you need to create a new instance of ArtifactStorage inside client
|
|
20
|
+
// so the solution is make it opposite to isMemory (which will be retrieved from global)
|
|
18
21
|
this.isFile = true;
|
|
19
22
|
this.isMemory = false;
|
|
20
|
-
this.tmpDirFullpath = this.createTestomatTmpDir(
|
|
23
|
+
this.tmpDirFullpath = this.createTestomatTmpDir(ArtifactStorage._tmpPrefix);
|
|
21
24
|
debug('SAVE to tmp folder mode enabled!');
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
static async storeToFile(tmpDirName, artifact, testSuffix = "test") {
|
|
29
|
+
// this assumes to use multiple files for each test. do we really want this?
|
|
26
30
|
const suffix = TESTOMAT_ARTIFACT_SUFFIX + uuid.v4() + testSuffix;
|
|
27
31
|
const dirpath = join(os.tmpdir(), tmpDirName);
|
|
32
|
+
// why json?
|
|
28
33
|
const filepath = resolve(dirpath, `${suffix}.json`);
|
|
29
34
|
|
|
30
35
|
return fs.promises.appendFile(filepath, JSON.stringify(artifact));
|
|
@@ -34,6 +39,7 @@ class ArtifactStorage {
|
|
|
34
39
|
// TODO: mode for different pre-hooks. As variant - "all-tests" || "only_fail"
|
|
35
40
|
// const mode = process.env.TESTOMAT_ARTIFACTS || "only_fail";
|
|
36
41
|
|
|
42
|
+
// you save the artifact without specifying its source - test id
|
|
37
43
|
if (Array.isArray(global.testomatioArtifacts)) {
|
|
38
44
|
debug("Saving artifacts to global storage");
|
|
39
45
|
|
|
@@ -71,6 +77,7 @@ class ArtifactStorage {
|
|
|
71
77
|
return list;
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
// returns all the content from all files; do we really need it?
|
|
74
81
|
async tmpContents() {
|
|
75
82
|
const contents = [];
|
|
76
83
|
|
|
@@ -103,8 +110,9 @@ class ArtifactStorage {
|
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// no need to use multiple dirs; we can implement it later if required
|
|
106
114
|
static tmpTestomatDirNames() {
|
|
107
|
-
const subname =
|
|
115
|
+
const subname = ArtifactStorage._tmpPrefix || "tsmt_reporter";
|
|
108
116
|
|
|
109
117
|
return fs.readdirSync(os.tmpdir(), { withFileTypes: true })
|
|
110
118
|
.filter((item) => item.isDirectory())
|
|
@@ -113,6 +121,7 @@ class ArtifactStorage {
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
cleanup() {
|
|
124
|
+
// when you do a cleanup, I know nothing about isFile param
|
|
116
125
|
if (this.isFile && this.tmpDirFullpath) {
|
|
117
126
|
const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
|
|
118
127
|
|
|
@@ -128,4 +137,6 @@ class ArtifactStorage {
|
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
ArtifactStorage._tmpPrefix = "tsmt_reporter";
|
|
141
|
+
|
|
131
142
|
module.exports = ArtifactStorage;
|
package/lib/adapter/jest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const TestomatClient = require('../client');
|
|
2
|
-
const { STATUS } = require('../constants');
|
|
3
|
-
const { parseTest, ansiRegExp } = require('../util');
|
|
2
|
+
const { STATUS, TESTOMAT_TMP_STORAGE } = require('../constants');
|
|
3
|
+
const { parseTest, ansiRegExp, fileSystem } = require('../util');
|
|
4
4
|
|
|
5
5
|
class JestReporter {
|
|
6
6
|
constructor(globalConfig, options) {
|
|
@@ -11,6 +11,16 @@ class JestReporter {
|
|
|
11
11
|
this.client.createRun();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
static getIdOfCurrentlyRunningTest() {
|
|
15
|
+
// @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
|
|
16
|
+
return parseTest(expect?.getState()?.currentTestName) || null; // eslint-disable-line
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onRunStart() {
|
|
20
|
+
// clear tmp dir
|
|
21
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
onTestResult(test, testResult) {
|
|
15
25
|
if (!this.client) return;
|
|
16
26
|
|
|
@@ -47,6 +57,9 @@ class JestReporter {
|
|
|
47
57
|
const { numFailedTests } = results;
|
|
48
58
|
const status = numFailedTests === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
49
59
|
this.client.updateRunStatus(status);
|
|
60
|
+
|
|
61
|
+
// clear tmp dir
|
|
62
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
package/lib/adapter/mocha.js
CHANGED
|
@@ -5,7 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const TestomatClient = require('../client');
|
|
6
6
|
const { STATUS } = require('../constants');
|
|
7
7
|
const { parseTest, specificTestInfo } = require('../util');
|
|
8
|
-
const ArtifactStorage = require('../
|
|
8
|
+
const ArtifactStorage = require('../_ArtifactStorageOld');
|
|
9
9
|
|
|
10
10
|
const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } = Mocha.Runner.constants;
|
|
11
11
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// const debug = require('debug')('@testomatio/reporter:logger');
|
|
2
|
+
const { DataStorage } = require('./dataStorage');
|
|
3
|
+
|
|
4
|
+
class ArtifactStorage {
|
|
5
|
+
constructor(params = {}) {
|
|
6
|
+
this.dataStorage = new DataStorage('artifact', params);
|
|
7
|
+
|
|
8
|
+
// singleton
|
|
9
|
+
if (!ArtifactStorage.instance) {
|
|
10
|
+
ArtifactStorage.instance = this;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
save(data, context = null) {
|
|
15
|
+
this.dataStorage.putData(data, context);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(context) {
|
|
19
|
+
this.dataStorage.getData(context);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ArtifactStorage.instance = null;
|
|
24
|
+
|
|
25
|
+
module.exports = new ArtifactStorage();
|
package/lib/client.js
CHANGED
|
@@ -7,6 +7,7 @@ const { randomUUID } = require('crypto');
|
|
|
7
7
|
const upload = require('./fileUploader');
|
|
8
8
|
const { APP_PREFIX } = require('./constants');
|
|
9
9
|
const pipesFactory = require('./pipe');
|
|
10
|
+
const logger = require('./logger');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {import('../types').TestData} TestData
|
|
@@ -47,6 +48,7 @@ class Client {
|
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
global.testomatioArtifacts = [];
|
|
51
|
+
// TODO: create global storage
|
|
50
52
|
|
|
51
53
|
this.queue = this.queue
|
|
52
54
|
.then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))))
|
|
@@ -58,7 +60,7 @@ class Client {
|
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* Updates test status and its data
|
|
61
|
-
*
|
|
63
|
+
*
|
|
62
64
|
* @param {string|undefined} status
|
|
63
65
|
* @param {TestData} [testData]
|
|
64
66
|
* @param {string[]} [storeArtifacts]
|
|
@@ -68,10 +70,11 @@ class Client {
|
|
|
68
70
|
// all pipes disabled, skipping
|
|
69
71
|
if (!this.pipes.filter(p => p.isEnabled).length) return;
|
|
70
72
|
|
|
71
|
-
if (!testData)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if (!testData)
|
|
74
|
+
testData = {
|
|
75
|
+
title: 'Unknown test',
|
|
76
|
+
suite_title: 'Unknown suite',
|
|
77
|
+
};
|
|
75
78
|
|
|
76
79
|
const {
|
|
77
80
|
error = null,
|
|
@@ -100,13 +103,17 @@ class Client {
|
|
|
100
103
|
stack = this.formatSteps(stack, steps);
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
const testLogs = logger.getLogs(test_id);
|
|
107
|
+
debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
108
|
+
stack += testLogs;
|
|
109
|
+
|
|
103
110
|
if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
|
|
104
|
-
debug(
|
|
111
|
+
debug('CLIENT storeArtifact', storeArtifacts);
|
|
105
112
|
files.push(...storeArtifacts);
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
if (Array.isArray(global.testomatioArtifacts)) {
|
|
109
|
-
debug(
|
|
116
|
+
debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
|
|
110
117
|
files.push(...global.testomatioArtifacts);
|
|
111
118
|
global.testomatioArtifacts = [];
|
|
112
119
|
}
|
|
@@ -172,8 +179,8 @@ class Client {
|
|
|
172
179
|
this.queue = this.queue
|
|
173
180
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
174
181
|
.then(() => {
|
|
175
|
-
debug(
|
|
176
|
-
|
|
182
|
+
debug('TOTAL uploaded files', this.totalUploaded);
|
|
183
|
+
|
|
177
184
|
if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
|
|
178
185
|
console.log(
|
|
179
186
|
APP_PREFIX,
|
package/lib/constants.js
CHANGED
|
@@ -3,6 +3,10 @@ const chalk = require('chalk');
|
|
|
3
3
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
4
4
|
const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
|
|
5
5
|
|
|
6
|
+
const TESTOMAT_TMP_STORAGE = {
|
|
7
|
+
mainDir: "testomatio_tmp",
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
const CSV_HEADERS = [
|
|
7
11
|
{ id: 'suite_title', title: 'Suite_title' },
|
|
8
12
|
{ id: 'title', title: 'Title' },
|
|
@@ -21,6 +25,7 @@ const STATUS = {
|
|
|
21
25
|
module.exports = {
|
|
22
26
|
APP_PREFIX,
|
|
23
27
|
TESTOMAT_ARTIFACT_SUFFIX,
|
|
28
|
+
TESTOMAT_TMP_STORAGE,
|
|
24
29
|
CSV_HEADERS,
|
|
25
30
|
STATUS,
|
|
26
31
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:storage');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { join } = require('path');
|
|
6
|
+
const JestReporter = require('./adapter/jest');
|
|
7
|
+
const { TESTOMAT_TMP_STORAGE } = require('./constants');
|
|
8
|
+
const { fileSystem } = require('./util');
|
|
9
|
+
|
|
10
|
+
class DataStorage {
|
|
11
|
+
/**
|
|
12
|
+
* Creates data storage instance for specific data type.
|
|
13
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner.
|
|
14
|
+
* dataType: 'log' | 'artifact' | ...
|
|
15
|
+
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
16
|
+
* @param {*} params
|
|
17
|
+
*/
|
|
18
|
+
constructor(dataType, params = {}) {
|
|
19
|
+
if (!dataType) throw new Error('Data type is required when creating data storage');
|
|
20
|
+
this.dataType = dataType || 'data';
|
|
21
|
+
this.dataDirName = `${dataType}s`;
|
|
22
|
+
this.isFileStorage = params?.isFileStorage ?? !global.testomatioDataStore;
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
FYI:
|
|
26
|
+
If this storage instance is used within test runner, it works fine.
|
|
27
|
+
But if, for example, we create storage instance inside the testomatio client (to get stored data),
|
|
28
|
+
the environment could be different (not already a test runner env).
|
|
29
|
+
Thus, checking environment is only reasonable when you put data to starage
|
|
30
|
+
and potentially useless when get data from storage
|
|
31
|
+
*/
|
|
32
|
+
const runningEnvironment = this.getRunningEnviroment();
|
|
33
|
+
if (runningEnvironment === 'jest') this.isFileStorage = true;
|
|
34
|
+
|
|
35
|
+
if (this.isFileStorage) {
|
|
36
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataDirName);
|
|
37
|
+
fileSystem.createDir(this.dataDirPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// TODO: implement
|
|
44
|
+
getTestIdFromContext(context) { // eslint-disable-line
|
|
45
|
+
// eslint-disable-line
|
|
46
|
+
// return testId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Try to define the running environment. Not 100% reliable and used as additional check
|
|
51
|
+
* @returns jest | mocha | ...
|
|
52
|
+
*/
|
|
53
|
+
getRunningEnviroment() {
|
|
54
|
+
if (process.env.JEST_WORKER_ID) return 'jest';
|
|
55
|
+
// @ts-expect-error mocha could be undefined, its ok
|
|
56
|
+
if (typeof mocha !== 'undefined') return 'mocha';
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Puts any data to storage (file or global variable)
|
|
62
|
+
* @param {*} data anything you want to store
|
|
63
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
64
|
+
* @returns
|
|
65
|
+
*/
|
|
66
|
+
putData(data, context = null) {
|
|
67
|
+
let testId = null;
|
|
68
|
+
if (typeof context === 'string') {
|
|
69
|
+
testId = context;
|
|
70
|
+
} else {
|
|
71
|
+
// TODO: derive testId from context
|
|
72
|
+
// testId = context...
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// try to get testId for Jest
|
|
76
|
+
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
77
|
+
|
|
78
|
+
// if testId is not provided, data is be saved to `{dataType}_other` file;
|
|
79
|
+
if (!testId) testId = 'other';
|
|
80
|
+
|
|
81
|
+
if (this.isFileStorage) {
|
|
82
|
+
this._putDataToFile(data, testId);
|
|
83
|
+
} else {
|
|
84
|
+
this._putDataToGlobalVar(data, testId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns data, stored for specific testId (or data which was stored without test id specified)
|
|
90
|
+
*
|
|
91
|
+
* (Don't try to guess the execution environment (e.g. test runner) inside this method, it could be any)
|
|
92
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
getData(context) {
|
|
96
|
+
let testId = null;
|
|
97
|
+
if (typeof context === 'string') {
|
|
98
|
+
testId = context;
|
|
99
|
+
} else {
|
|
100
|
+
// TODO: derive testId from context
|
|
101
|
+
// testId = context...
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!testId) testId = 'other';
|
|
105
|
+
|
|
106
|
+
if (this.isFileStorage) {
|
|
107
|
+
return this._getDataFromFile(testId);
|
|
108
|
+
}
|
|
109
|
+
if (global?.testomatioDataStore) {
|
|
110
|
+
return this._getDataFromGlobalVar(testId);
|
|
111
|
+
}
|
|
112
|
+
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_getDataFromGlobalVar(testId) {
|
|
117
|
+
try {
|
|
118
|
+
if (global?.testomatioDataStore[this.dataDirName]) {
|
|
119
|
+
const testData = global.testomatioDataStore[this.dataDirName][testId];
|
|
120
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
121
|
+
return testData;
|
|
122
|
+
}
|
|
123
|
+
debug(`No ${this.dataType} data for test id ${testId}`);
|
|
124
|
+
return null;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// there could be no data, ignore
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_getDataFromFile(testId) {
|
|
131
|
+
try {
|
|
132
|
+
const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, `${this.dataType}_${testId}`);
|
|
133
|
+
if (fs.existsSync(filepath)) {
|
|
134
|
+
const testData = fs.readFileSync(filepath, 'utf-8');
|
|
135
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
136
|
+
return testData;
|
|
137
|
+
}
|
|
138
|
+
debug(`No ${this.dataType} data for test id ${testId}`);
|
|
139
|
+
return null;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// there could be no data, ignore
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_putDataToGlobalVar(data, testId) {
|
|
147
|
+
debug('Saving data to global variable for test', testId, ':\n', data, '\n');
|
|
148
|
+
global.testomatioDataStore[this.dataDirName] = {};
|
|
149
|
+
global.testomatioDataStore[this.dataDirName][testId] = data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_putDataToFile(data, testId) {
|
|
153
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
154
|
+
const filename = `${this.dataType}_${testId}`;
|
|
155
|
+
const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, filename);
|
|
156
|
+
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
157
|
+
|
|
158
|
+
// TODO: handle multiple invocations of JSON.stringify.
|
|
159
|
+
// UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
|
|
160
|
+
fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports.DataStorage = DataStorage;
|
|
165
|
+
|
|
166
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
167
|
+
// prevent blocking and improve performance (probably queue usage will be required)
|
|
168
|
+
|
|
169
|
+
// TODO: rewrite client - everything regarding storing artifacts
|
|
170
|
+
// TODO: try to define adapter inside client
|
|
171
|
+
// TODO: use .env
|
|
172
|
+
// TODO: ability to intercept multiple loggers
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const debug = require('debug')('@testomatio/reporter:logger');
|
|
3
|
+
const { DataStorage } = require('./dataStorage');
|
|
4
|
+
|
|
5
|
+
const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
|
|
6
|
+
const LEVELS = {
|
|
7
|
+
ALL: { severity: 1, color: '' },
|
|
8
|
+
VERBOSE: { severity: 3, color: 'grey' },
|
|
9
|
+
TRACE: { severity: 5, color: 'grey' },
|
|
10
|
+
DEBUG: { severity: 7, color: 'cyan' },
|
|
11
|
+
INFO: { severity: 9, color: 'black' },
|
|
12
|
+
LOG: { severity: 11, color: 'black' },
|
|
13
|
+
WARN: { severity: 13, color: 'yellow' },
|
|
14
|
+
ERROR: { severity: 15, color: 'red' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Logger allows to:
|
|
19
|
+
* 1. Intercept logs from user logger (console.log, etc) and store them.
|
|
20
|
+
* 2. Output logs to console (actually, logger functionality).
|
|
21
|
+
* 3. Varied syntax.
|
|
22
|
+
*/
|
|
23
|
+
class Logger {
|
|
24
|
+
// _originalUserLogger used to output logs to console by the user logger
|
|
25
|
+
// _loggerToIntercept intercepted and reassigned immediately when added
|
|
26
|
+
|
|
27
|
+
constructor(params = {}) {
|
|
28
|
+
// set default logger to be used in log, warn, error, etc methods
|
|
29
|
+
this._originalUserLogger = { ...console };
|
|
30
|
+
|
|
31
|
+
this.dataStorage = new DataStorage('log', params);
|
|
32
|
+
this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
33
|
+
|
|
34
|
+
// commented because prefer to use "intercept" method
|
|
35
|
+
// if (params?.logger) this._loggerToIntercept = params.logger;
|
|
36
|
+
|
|
37
|
+
this.intercept(this._loggerToIntercept);
|
|
38
|
+
|
|
39
|
+
// singleton
|
|
40
|
+
if (!Logger.instance) {
|
|
41
|
+
Logger.instance = this;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the string (logs) and the context
|
|
47
|
+
* This is required because loggers take multiple arguments
|
|
48
|
+
* @param {...any} args
|
|
49
|
+
*/
|
|
50
|
+
/*
|
|
51
|
+
TODO: its difficult to distinguish context from log artument if its not a string,
|
|
52
|
+
e.g. user can use something like console.log('some log', { key: 'value' });
|
|
53
|
+
consider not to use this method at all and don't expect context from user inside default log methods;
|
|
54
|
+
instead, use custom method like $`some log` and parse it
|
|
55
|
+
*/
|
|
56
|
+
// _getLogStringAndContextFromArgs(...args) {
|
|
57
|
+
// let context = '';
|
|
58
|
+
// if (args.length > 1 && typeof args[args.length - 1] !== 'string') {
|
|
59
|
+
// context = args.pop();
|
|
60
|
+
// }
|
|
61
|
+
// const logs = args.join(' ');
|
|
62
|
+
// return { logs, context };
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Tagget template literal. Allows to use different syntaxes:
|
|
67
|
+
* 1. Tagget template: $`text ${someVar}`
|
|
68
|
+
* 2. Standard: $(`text ${someVar}`)
|
|
69
|
+
* 3. Standard with multiple arguments: $('text', someVar)
|
|
70
|
+
*/
|
|
71
|
+
_log(strings, ...args) {
|
|
72
|
+
let logs;
|
|
73
|
+
// this block means tagged template is used (syntax like $`text ${someVar}`)
|
|
74
|
+
if (Array.isArray(strings) && strings.length === args.length + 1) {
|
|
75
|
+
logs = strings.reduce(
|
|
76
|
+
(result, current, index) =>
|
|
77
|
+
result +
|
|
78
|
+
current +
|
|
79
|
+
// strings are splitted by args when use tagged template, thus we add arg after each string
|
|
80
|
+
// it looks like: `string1 arg1 string2 arg2 string3`
|
|
81
|
+
(args[index] !== undefined // eslint-disable-line no-nested-ternary
|
|
82
|
+
? typeof args[index] === 'string'
|
|
83
|
+
? args[index] // add arg as it is
|
|
84
|
+
: this._strinfifyLogs(args[index]) // stringify arg
|
|
85
|
+
: ' '), // add space if no arg after string
|
|
86
|
+
// initial accumulator value
|
|
87
|
+
'',
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
// this block means arguments syntax is used (syntax like $('text', someVar))
|
|
91
|
+
// in this case strings represents just a first argument
|
|
92
|
+
logs = this._strinfifyLogs(strings, ...args);
|
|
93
|
+
}
|
|
94
|
+
this.dataStorage.putData(logs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Allows you to define a step inside a test. Step name is attached to the report and
|
|
99
|
+
* helps to understand the test flow.
|
|
100
|
+
* @param {*} strings
|
|
101
|
+
* @param {...any} values
|
|
102
|
+
*/
|
|
103
|
+
step(strings, ...values) {
|
|
104
|
+
let logs = '';
|
|
105
|
+
for (let i = 0; i < strings.length; i++) {
|
|
106
|
+
logs += strings[i];
|
|
107
|
+
if (i < values.length) {
|
|
108
|
+
logs += values[i];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
logs = chalk.blue(`> ${logs}`);
|
|
112
|
+
this.dataStorage.putData(logs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
* @param {*} context testId or test context from test runner
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
getLogs(context) {
|
|
121
|
+
return this.dataStorage.getData(context);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_strinfifyLogs(...args) {
|
|
125
|
+
const logs = [];
|
|
126
|
+
// stringify everything except strings
|
|
127
|
+
for (const arg of args) {
|
|
128
|
+
if (typeof arg === 'string') {
|
|
129
|
+
logs.push(arg);
|
|
130
|
+
} else {
|
|
131
|
+
try {
|
|
132
|
+
// eslint-disable-next-line no-unused-expressions
|
|
133
|
+
this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
|
|
134
|
+
} catch (e) {
|
|
135
|
+
debug('Error while stringify object', e);
|
|
136
|
+
logs.push(arg);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return logs.join(' ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
assert(...args) {
|
|
144
|
+
const level = 'ERROR';
|
|
145
|
+
const severity = LEVELS[level].severity;
|
|
146
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
147
|
+
|
|
148
|
+
const logs = this._strinfifyLogs(...args);
|
|
149
|
+
this.dataStorage.putData(logs);
|
|
150
|
+
try {
|
|
151
|
+
this._originalUserLogger.log(`Assertion result: `, ...args);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// method could be unexisting, ignore error
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
debug(...args) {
|
|
158
|
+
const level = 'DEBUG';
|
|
159
|
+
const severity = LEVELS[level].severity;
|
|
160
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
161
|
+
|
|
162
|
+
if (this.logLevel === 'error' || this.logLevel === 'warn') return;
|
|
163
|
+
const logs = this._strinfifyLogs(...args);
|
|
164
|
+
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
165
|
+
this.dataStorage.putData(colorizedLogs);
|
|
166
|
+
try {
|
|
167
|
+
this._originalUserLogger.debug(...args);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// method could be unexisting, ignore error
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
error(...args) {
|
|
174
|
+
const level = 'ERROR';
|
|
175
|
+
const severity = LEVELS[level].severity;
|
|
176
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
177
|
+
|
|
178
|
+
const logs = this._strinfifyLogs(...args);
|
|
179
|
+
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
180
|
+
this.dataStorage.putData(colorizedLogs);
|
|
181
|
+
try {
|
|
182
|
+
this._originalUserLogger.error(...args);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// method could be unexisting, ignore error
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
info(...args) {
|
|
189
|
+
const level = 'INFO';
|
|
190
|
+
const severity = LEVELS[level].severity;
|
|
191
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
192
|
+
|
|
193
|
+
const logs = this._strinfifyLogs(...args);
|
|
194
|
+
this.dataStorage.putData(logs);
|
|
195
|
+
try {
|
|
196
|
+
this._originalUserLogger.info(...args);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// method could be unexisting, ignore error
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
log(...args) {
|
|
203
|
+
const level = 'INFO';
|
|
204
|
+
const severity = LEVELS[level].severity;
|
|
205
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
206
|
+
|
|
207
|
+
const logs = this._strinfifyLogs(...args);
|
|
208
|
+
this.dataStorage.putData(logs);
|
|
209
|
+
try {
|
|
210
|
+
this._originalUserLogger.log(...args);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// method could be unexisting, ignore error
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
trace(...args) {
|
|
217
|
+
const level = 'TRACE';
|
|
218
|
+
const severity = LEVELS[level].severity;
|
|
219
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
220
|
+
|
|
221
|
+
const logs = this._strinfifyLogs(...args);
|
|
222
|
+
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
223
|
+
this.dataStorage.putData(colorizedLogs);
|
|
224
|
+
try {
|
|
225
|
+
this._originalUserLogger.trace(...args);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// method could be unexisting, ignore error
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
warn(...args) {
|
|
232
|
+
const level = 'WARN';
|
|
233
|
+
const severity = LEVELS[level].severity;
|
|
234
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
235
|
+
|
|
236
|
+
const logs = this._strinfifyLogs(...args);
|
|
237
|
+
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
238
|
+
this.dataStorage.putData(colorizedLogs);
|
|
239
|
+
try {
|
|
240
|
+
this._originalUserLogger.warn(...args);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// method could be unexisting, ignore error
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Intercepts user logger messages.
|
|
248
|
+
* When call this method, Logger start to control the user logger,
|
|
249
|
+
* but almost nothing is changed for user ragarding the console output (like log level set by user)
|
|
250
|
+
* (until multiple loggers are intercepted,
|
|
251
|
+
* in this case only the last intercepted logger will be used as user console output).
|
|
252
|
+
* @param {*} userLogger
|
|
253
|
+
*/
|
|
254
|
+
intercept(userLogger) {
|
|
255
|
+
if (!userLogger) return;
|
|
256
|
+
debug(`Intercepting user logger`);
|
|
257
|
+
|
|
258
|
+
// save original user logger to use it for logging (last intercepted will be used for console output)
|
|
259
|
+
this._originalUserLogger = { ...userLogger };
|
|
260
|
+
this._loggerToIntercept = userLogger;
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
override user logger (any, e.g. console) methods to intercept log messages
|
|
264
|
+
this._loggerToIntercept = this; could be used, but decided to override only output methods
|
|
265
|
+
*/
|
|
266
|
+
for (const method of LOG_METHODS) {
|
|
267
|
+
/*
|
|
268
|
+
decided to comment next code line because its better to create method even if it does not exist in user logger;
|
|
269
|
+
on method invocation, we will store the data anyway and catch block will prevent potential errors
|
|
270
|
+
*/
|
|
271
|
+
// if (!this._loggerToIntercept[method]) continue;
|
|
272
|
+
this._loggerToIntercept[method] = (...args) => this[method](...args);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Allows to configure logger. Make sure you do it before the logger usage in your code.
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} [config={}] - The configuration object.
|
|
280
|
+
* @param {string} [config.logLevel] - The desired log level. Valid values are 'DEBUG', 'INFO', 'WARN', and 'ERROR'.
|
|
281
|
+
* @param {boolean} [config.prettyObjects] - Specifies whether to enable pretty printing of objects.
|
|
282
|
+
* @returns {void}
|
|
283
|
+
*/
|
|
284
|
+
configure(config = {}) {
|
|
285
|
+
if (!config) return;
|
|
286
|
+
if (config.prettyObjects) this.prettyObjects = config.prettyObjects;
|
|
287
|
+
if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Logger.instance = null;
|
|
292
|
+
|
|
293
|
+
// module.exports.Logger = Logger;
|
|
294
|
+
module.exports = new Logger();
|
|
295
|
+
|
|
296
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to prevent blocking and improve performance
|
|
297
|
+
// TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
|
|
298
|
+
// upd: did not face such loggers, but still could be useful
|
|
299
|
+
|
|
300
|
+
// TODO: in case of unset _originalUserLogger, logger.{method} will not provide console output,
|
|
301
|
+
// need to add some logger by default
|
package/lib/reporter.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
const logger = require('./logger');
|
|
1
2
|
const TestomatClient = require('./client');
|
|
2
3
|
const TRConstants = require('./constants');
|
|
3
|
-
const TRArtifacts = require('./
|
|
4
|
+
const TRArtifacts = require('./_ArtifactStorageOld');
|
|
5
|
+
|
|
6
|
+
const log = logger._log.bind(logger);
|
|
7
|
+
const step = logger.step.bind(logger);
|
|
4
8
|
|
|
5
9
|
module.exports = {
|
|
10
|
+
logger,
|
|
11
|
+
log,
|
|
12
|
+
step,
|
|
6
13
|
TestomatClient,
|
|
7
14
|
TRConstants,
|
|
8
15
|
TRArtifacts,
|
package/lib/util.js
CHANGED
|
@@ -3,6 +3,7 @@ const { sep, basename } = require('path');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const isValid = require('is-valid-path');
|
|
6
|
+
const debug = require('debug')('@testomatio/reporter:util');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @param {String} testTitle - Test title
|
|
@@ -32,7 +33,6 @@ const parseSuite = suiteTitle => {
|
|
|
32
33
|
return null;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
|
|
36
36
|
const ansiRegExp = () => {
|
|
37
37
|
const pattern = [
|
|
38
38
|
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
@@ -54,11 +54,14 @@ const isValidUrl = s => {
|
|
|
54
54
|
|
|
55
55
|
const fetchFilesFromStackTrace = (stack = '') => {
|
|
56
56
|
const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
|
|
57
|
-
return Array.from(files)
|
|
58
|
-
|
|
57
|
+
return Array.from(files)
|
|
58
|
+
.map(f => f[1])
|
|
59
|
+
.filter(f => fs.existsSync(f));
|
|
60
|
+
};
|
|
59
61
|
|
|
60
62
|
const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
61
|
-
const stackLines = stack
|
|
63
|
+
const stackLines = stack
|
|
64
|
+
.split('\n')
|
|
62
65
|
.filter(l => l.includes(':'))
|
|
63
66
|
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
64
67
|
// .map(l => l.split(':')[0])
|
|
@@ -67,17 +70,17 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
67
70
|
.filter(l => isValid(l?.split(':')[0]))
|
|
68
71
|
|
|
69
72
|
// // filter out 3rd party libs
|
|
70
|
-
.filter(l => !l?.includes(`vendor${
|
|
71
|
-
.filter(l => !l?.includes(`node_modules${
|
|
73
|
+
.filter(l => !l?.includes(`vendor${sep}`))
|
|
74
|
+
.filter(l => !l?.includes(`node_modules${sep}`))
|
|
72
75
|
.filter(l => fs.existsSync(l.split(':')[0]))
|
|
73
|
-
.filter(l => fs.lstatSync(l.split(':')[0]).isFile())
|
|
76
|
+
.filter(l => fs.lstatSync(l.split(':')[0]).isFile());
|
|
74
77
|
|
|
75
78
|
if (!stackLines.length) return '';
|
|
76
79
|
|
|
77
80
|
const [file, line] = stackLines[0].split(':');
|
|
78
81
|
|
|
79
82
|
const prepend = 3;
|
|
80
|
-
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
|
|
83
|
+
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
81
84
|
|
|
82
85
|
if (!source) return '';
|
|
83
86
|
|
|
@@ -85,22 +88,22 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
85
88
|
.map((l, i) => {
|
|
86
89
|
if (i === prepend) return `${line} > ${chalk.bold(l)}`;
|
|
87
90
|
return `${line - prepend + i} | ${l}`
|
|
88
|
-
}).join('\n')
|
|
89
|
-
}
|
|
91
|
+
}).join('\n');
|
|
92
|
+
};
|
|
90
93
|
|
|
91
94
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
92
95
|
if (!opts.title && !opts.line) return '';
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
// code fragment is 20 lines
|
|
95
98
|
const limit = opts.limit || 50;
|
|
96
99
|
let lineIndex;
|
|
97
100
|
if (opts.line) lineIndex = opts.line - 1;
|
|
98
|
-
const lines = contents.split('\n')
|
|
101
|
+
const lines = contents.split('\n');
|
|
99
102
|
|
|
100
103
|
// remove special chars from title
|
|
101
104
|
if (!lineIndex && opts.title) {
|
|
102
|
-
const title = opts.title.replace(/[([@].*/g, '')
|
|
103
|
-
lineIndex = lines.findIndex(l => l.includes(title))
|
|
105
|
+
const title = opts.title.replace(/[([@].*/g, '');
|
|
106
|
+
lineIndex = lines.findIndex(l => l.includes(title));
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
if (opts.prepend) {
|
|
@@ -133,52 +136,69 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
133
136
|
if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
|
|
134
137
|
if (opts.lang === 'java' && lines[i].includes(' class ')) break;
|
|
135
138
|
}
|
|
136
|
-
result.push(lines[i])
|
|
139
|
+
result.push(lines[i]);
|
|
137
140
|
}
|
|
138
141
|
return result.join('\n');
|
|
139
142
|
}
|
|
140
|
-
}
|
|
143
|
+
};
|
|
141
144
|
|
|
142
|
-
const isSameTest = (test, t) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const isSameTest = (test, t) =>
|
|
146
|
+
typeof t === 'object' &&
|
|
147
|
+
typeof test === 'object' &&
|
|
148
|
+
t.title === test.title &&
|
|
149
|
+
t.suite_title === test.suite_title &&
|
|
150
|
+
Object.values(t.example || {}) === Object.values(test.example || {}) &&
|
|
151
|
+
t.test_id === test.test_id;
|
|
148
152
|
|
|
149
153
|
const getCurrentDateTime = () => {
|
|
150
154
|
const today = new Date();
|
|
151
|
-
|
|
152
|
-
return `${today.getFullYear()
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
+
|
|
156
|
+
return `${today.getFullYear()}_${
|
|
157
|
+
today.getMonth() + 1
|
|
158
|
+
}_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
|
|
159
|
+
};
|
|
155
160
|
|
|
156
161
|
/**
|
|
157
162
|
* @param {Object} test - Test adapter object
|
|
158
163
|
*
|
|
159
|
-
* @returns {String|null} testInfo as one string
|
|
164
|
+
* @returns {String|null} testInfo as one string
|
|
160
165
|
*/
|
|
161
166
|
const specificTestInfo = test => {
|
|
162
167
|
// TODO: afterEach has another context.... need to add specific handler, maybe...
|
|
163
168
|
if (test?.title && test?.file) {
|
|
164
|
-
|
|
165
|
-
return `${basename(test.file).split(".").join("#")
|
|
166
|
-
}#${
|
|
167
|
-
test.title.split(" ").join("#")}`;
|
|
169
|
+
return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
return null;
|
|
171
173
|
};
|
|
172
174
|
|
|
175
|
+
const fileSystem = {
|
|
176
|
+
createDir(dirPath) {
|
|
177
|
+
if (!fs.existsSync(dirPath)) {
|
|
178
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
179
|
+
debug('Created dir: ', dirPath);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
clearDir(dirPath) {
|
|
183
|
+
if (fs.existsSync(dirPath)) {
|
|
184
|
+
fs.rmSync(dirPath, { recursive: true });
|
|
185
|
+
debug(`Dir ${dirPath} was deleted`);
|
|
186
|
+
} else {
|
|
187
|
+
debug(`Trying to delete ${dirPath} but it doesn't exist`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
173
192
|
module.exports = {
|
|
174
193
|
isSameTest,
|
|
175
194
|
fetchSourceCode,
|
|
176
195
|
fetchSourceCodeFromStackTrace,
|
|
177
196
|
fetchFilesFromStackTrace,
|
|
197
|
+
fileSystem,
|
|
178
198
|
getCurrentDateTime,
|
|
179
199
|
specificTestInfo,
|
|
180
200
|
isValidUrl,
|
|
181
201
|
ansiRegExp,
|
|
182
202
|
parseTest,
|
|
183
|
-
parseSuite
|
|
184
|
-
}
|
|
203
|
+
parseSuite,
|
|
204
|
+
};
|
package/package.json
CHANGED
package/Changelog.md
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
# 1.0.0
|
|
2
|
-
|
|
3
|
-
<!-- pending release updates -->
|
|
4
|
-
|
|
5
|
-
* Added [`TESTOMATIO_SHARED_RUN` option](https://github.com/testomatio/reporter/blob/master/docs/pipes.md#reporting-parallel-execution-to-to-same-run) to use a shared run for parallel executions
|
|
6
|
-
* Reworked [documentation](https://github.com/testomatio/reporter/tree/master#readme).
|
|
7
|
-
* Added an option to obtain [S3 configuration](https://github.com/testomatio/reporter/blob/master/docs/artifacts.md#configuration) from Testomat.io
|
|
8
|
-
* Introduced [pipes](https://github.com/testomatio/reporter/blob/master/docs/pipes.md):
|
|
9
|
-
* GitHub
|
|
10
|
-
* GitLab
|
|
11
|
-
* CSV Pipe
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# 0.7.6
|
|
15
|
-
|
|
16
|
-
* Updated to use AWS S3 3.0 SDK for uploading
|
|
17
|
-
|
|
18
|
-
# 0.7.5
|
|
19
|
-
|
|
20
|
-
* Fixed reporting skipped tests in mocha
|
|
21
|
-
|
|
22
|
-
# 0.7.4
|
|
23
|
-
|
|
24
|
-
* Fixed parsing source code in JUnit files
|
|
25
|
-
|
|
26
|
-
# 0.7.3
|
|
27
|
-
|
|
28
|
-
* CodeceptJS: Upload all traces and videos from artifacts
|
|
29
|
-
* Fixed reporting skipped test in XML
|
|
30
|
-
* added `--timelimit` option to `report-xml` command line
|
|
31
|
-
|
|
32
|
-
# 0.7.2
|
|
33
|
-
|
|
34
|
-
* Fixed uploading non-existing file
|
|
35
|
-
|
|
36
|
-
# 0.7.1
|
|
37
|
-
|
|
38
|
-
* Support for NUnit XML v3 format
|
|
39
|
-
|
|
40
|
-
# 0.7.0
|
|
41
|
-
|
|
42
|
-
* Support for `@cucumber/cucumber` (>= 7.0) added
|
|
43
|
-
* Initial support for C# and NUnit
|
|
44
|
-
|
|
45
|
-
# 0.6.10
|
|
46
|
-
|
|
47
|
-
* Fixed uploading multilpe artifacts in Playwright
|
|
48
|
-
|
|
49
|
-
# 0.6.9
|
|
50
|
-
|
|
51
|
-
* Fixed pending tests reports for Cypress
|
|
52
|
-
|
|
53
|
-
# 0.6.8
|
|
54
|
-
# 0.6.7
|
|
55
|
-
|
|
56
|
-
* Pytest: fixed creating suites from reports
|
|
57
|
-
|
|
58
|
-
# 0.6.6
|
|
59
|
-
|
|
60
|
-
* JUnit reporter: prefer suite title over testcase classname in a report
|
|
61
|
-
|
|
62
|
-
# 0.6.5
|
|
63
|
-
|
|
64
|
-
* Fixed test statuses for runs in JUnit reporter
|
|
65
|
-
|
|
66
|
-
# 0.6.4
|
|
67
|
-
|
|
68
|
-
* Added `TESTOMATIO_PROCEED=1` param to not close current run
|
|
69
|
-
* Fixed priority of commands from `npx @testomatio/reporter`
|
|
70
|
-
|
|
71
|
-
# 0.6.3
|
|
72
|
-
|
|
73
|
-
* Fixed `npx start-test-run` to launch commands
|
|
74
|
-
|
|
75
|
-
# 0.6.2
|
|
76
|
-
|
|
77
|
-
* Added `--env-file` option to load env variables from env file
|
|
78
|
-
|
|
79
|
-
# 0.6.1
|
|
80
|
-
|
|
81
|
-
* Fixed creating RunGroup with JUnit reporter
|
|
82
|
-
|
|
83
|
-
# 0.6.0
|
|
84
|
-
|
|
85
|
-
* JUnit reporter support
|
|
86
|
-
|
|
87
|
-
# 0.5.10
|
|
88
|
-
|
|
89
|
-
* Fixed reporting Scenario Outline in Cypress-Cucumber
|
|
90
|
-
* Fixed error reports for Cypress when running in Chrome
|
|
91
|
-
|
|
92
|
-
# 0.5.9
|
|
93
|
-
|
|
94
|
-
* Added environment on Cypress report
|
|
95
|
-
|
|
96
|
-
# 0.5.8
|
|
97
|
-
|
|
98
|
-
* Fixed Cypress.io reporting
|
|
99
|
-
|
|
100
|
-
# 0.5.7
|
|
101
|
-
|
|
102
|
-
* Fixed webdriverio artifacts
|
|
103
|
-
|
|
104
|
-
# 0.5.6
|
|
105
|
-
|
|
106
|
-
* Unmark failed CodeceptJS tests as skipped
|
|
107
|
-
|
|
108
|
-
# 0.5.5
|
|
109
|
-
|
|
110
|
-
* Fixed `BeforeSuite` failures in CodeceptJS
|
|
111
|
-
|
|
112
|
-
# 0.5.4
|
|
113
|
-
|
|
114
|
-
Added `TESTOMATIO_CREATE=1` option to create unmatched tests on report
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
TESTOMATIO_CREATE=1 TESTOMATIO=apiKey npx codeceptjs run
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
# 0.5.3
|
|
121
|
-
|
|
122
|
-
* Fixed parsing suites
|
|
123
|
-
|
|
124
|
-
# 0.5.2
|
|
125
|
-
|
|
126
|
-
* Fixed multiple upload of artifacts in Cypress.io
|
|
127
|
-
|
|
128
|
-
# 0.5.1
|
|
129
|
-
|
|
130
|
-
* Fixed Cypress.io to report tests inside nested suites
|
|
131
|
-
|
|
132
|
-
# 0.5.0
|
|
133
|
-
|
|
134
|
-
* Added Cypress.io plugin
|
|
135
|
-
* Added artifacts upload to webdriverio
|
|
136
|
-
|
|
137
|
-
# 0.4.6
|
|
138
|
-
|
|
139
|
-
- Fixed CodeceptJS reporter to report tests failed in hooks
|
|
140
|
-
|
|
141
|
-
# 0.4.5
|
|
142
|
-
|
|
143
|
-
- Fixed "Total XX artifacts publicly uploaded to S3 bucket" when no S3 bucket is configured
|
|
144
|
-
- Improved S3 connection error messages
|
|
145
|
-
|
|
146
|
-
# 0.4.4
|
|
147
|
-
|
|
148
|
-
- Fixed returning 0 exit code when a process fails when running tests in parallel via `start-test-run`. Previously was using the last exit code returned by a process. Currently prefers the highest exit code that was returned by a process.
|
|
149
|
-
|
|
150
|
-
# 0.4.3
|
|
151
|
-
|
|
152
|
-
- Added `TESTOMATIO_DISABLE_ARTIFACTS` env variable to disable publishing artifacts.
|
|
153
|
-
|
|
154
|
-
# 0.4.2
|
|
155
|
-
|
|
156
|
-
- print version of reporter
|
|
157
|
-
- print number of uploaded artifacts
|
|
158
|
-
- print access mode for uploaded artifacts
|
|
159
|
-
|
|
160
|
-
# 0.4.1
|
|
161
|
-
|
|
162
|
-
Added `global.testomatioArtifacts = []` array which can be used to add arbitrary artifacts to a report.
|
|
163
|
-
|
|
164
|
-
```js
|
|
165
|
-
// inside a running test:
|
|
166
|
-
global.testomatioArtifacts.push('file/to/upload.png');
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
# 0.4.0
|
|
170
|
-
|
|
171
|
-
- Playwright: Introduced playwright/test support with screenshots and video artifacts
|
|
172
|
-
|
|
173
|
-
> Known issues: reporting using projects configured in Playwright does not work yet
|
|
174
|
-
|
|
175
|
-
- CodeceptJS: added video uploads
|
|
176
|
-
|
|
177
|
-
# 0.3.16
|
|
178
|
-
|
|
179
|
-
- CodeceptJS: fixed reporting tests with empty steps (on retry)
|
|
180
|
-
|
|
181
|
-
# 0.3.15
|
|
182
|
-
|
|
183
|
-
- Finish Run via API:
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
TESTOMATIO={apiKey} TESTOMATIO_RUN={runId} npx @testomatio/reporter@latest --finish
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
# 0.3.14
|
|
190
|
-
|
|
191
|
-
- Create an empty Run via API:
|
|
192
|
-
|
|
193
|
-
```
|
|
194
|
-
TESTOMATIO={apiKey} npx @testomatio/reporter@latest --launch
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
# 0.3.13
|
|
198
|
-
|
|
199
|
-
- Checking for a valid report URL
|
|
200
|
-
- Sending unlimited data on test report
|
|
201
|
-
|
|
202
|
-
# 0.3.12
|
|
203
|
-
|
|
204
|
-
- Fixed submitting arbitrary data on a test run
|
|
205
|
-
- Jest: fixed sending errors with stack traces
|
|
206
|
-
- Cypress: fixed sending reports
|
|
207
|
-
|
|
208
|
-
# 0.3.11
|
|
209
|
-
|
|
210
|
-
- Fixed circular JSON reference when submitting data to Testomatio
|
|
211
|
-
|
|
212
|
-
# 0.3.10
|
|
213
|
-
|
|
214
|
-
- Minor fixes
|
|
215
|
-
|
|
216
|
-
# 0.3.9
|
|
217
|
-
|
|
218
|
-
- Making all reporters to run without API key
|
|
219
|
-
|
|
220
|
-
# 0.3.8
|
|
221
|
-
|
|
222
|
-
- Fixed `npx start-test-run` to work with empty API keys
|
|
223
|
-
|
|
224
|
-
# 0.3.7
|
|
225
|
-
|
|
226
|
-
- Fixed release
|
|
227
|
-
|
|
228
|
-
# 0.3.6
|
|
229
|
-
|
|
230
|
-
- Update title and rungroup on start for scheduled runs.
|
|
231
|
-
|
|
232
|
-
# 0.3.5
|
|
233
|
-
|
|
234
|
-
- Added `TESTOMATIO_RUN` environment variable to pass id of a specific run to report
|
|
235
|
-
|
|
236
|
-
# 0.3.4
|
|
237
|
-
|
|
238
|
-
- Minor fixes
|
|
239
|
-
|
|
240
|
-
# 0.3.3
|
|
241
|
-
|
|
242
|
-
- [CodeceptJS] Fixed stack trace reporting
|
|
243
|
-
- [CodeceptJS] Fixed displaying of nested steps
|
|
244
|
-
- [CodeceptJS][mocha] Added assertion diff to report
|
|
245
|
-
|
|
246
|
-
# 0.3.2
|
|
247
|
-
|
|
248
|
-
- Fixed error message for S3 uploading
|
|
249
|
-
|
|
250
|
-
# 0.3.1
|
|
251
|
-
|
|
252
|
-
- [CodeceptJS] Better formatter for nested structures and BDD tests
|
|
253
|
-
|
|
254
|
-
# 0.3.0
|
|
255
|
-
|
|
256
|
-
- Added `TESTOMATIO_TITLE` env variable to set a name for Run
|
|
257
|
-
- Added `TESTOMATIO_RUNGROUP_TITLE` env variable to attach Run to RunGroup
|
|
258
|
-
- Added `TESTOMATIO_ENV` env variable to attach additional env values to report
|
|
259
|
-
- [CodeceptJS] **CodeceptJS v3 support**
|
|
260
|
-
- [CodeceptJS] Dropped support for CodeceptJS 2
|
|
261
|
-
- [CodeceptJS] Added support for before hooks
|
|
262
|
-
- [CodeceptJS] Log of steps
|
|
263
|
-
- [CodeceptJS] Upload screenshots of failed tests to S3
|
|
264
|
-
- [CodeceptJS] Updated to use with parallel execution
|