@testomatio/reporter 1.0.17 → 1.0.18-beta.1
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/Changelog.md +4 -0
- package/lib/adapter/codecept.js +2 -5
- package/lib/adapter/cucumber/current.js +3 -3
- package/lib/adapter/cucumber/legacy.js +2 -2
- package/lib/adapter/jest.js +2 -16
- package/lib/adapter/mocha.js +15 -43
- package/lib/adapter/playwright.js +33 -21
- package/lib/bin/startTest.js +6 -6
- package/lib/client.js +18 -21
- package/lib/constants.js +4 -6
- package/lib/reporter-functions.js +43 -0
- package/lib/reporter.js +9 -4
- package/lib/storages/artifact-storage.js +70 -0
- package/lib/{dataStorage.js → storages/data-storage.js} +122 -47
- package/lib/storages/key-value-storage.js +58 -0
- package/lib/{logger.js → storages/logger.js} +4 -33
- package/lib/utils/utils.js +14 -0
- package/package.json +1 -1
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
package/Changelog.md
CHANGED
package/lib/adapter/codecept.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const debug = require('debug')('@testomatio/reporter:adapter:codeceptjs');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const TestomatClient = require('../client');
|
|
4
|
-
const { STATUS, APP_PREFIX,
|
|
4
|
+
const { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
5
5
|
const upload = require('../fileUploader');
|
|
6
6
|
const { parseTest: getIdFromTestTitle, fileSystem } = require('../utils/utils');
|
|
7
7
|
|
|
@@ -56,7 +56,7 @@ function CodeceptReporter(config) {
|
|
|
56
56
|
// Listening to events
|
|
57
57
|
event.dispatcher.on(event.all.before, () => {
|
|
58
58
|
// clear tmp dir
|
|
59
|
-
fileSystem.clearDir(
|
|
59
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
60
60
|
|
|
61
61
|
recorder.add('Creating new run', () => client.createRun());
|
|
62
62
|
videos = [];
|
|
@@ -147,9 +147,6 @@ function CodeceptReporter(config) {
|
|
|
147
147
|
failedTests.push(id || title);
|
|
148
148
|
let testId = parseTest(tags);
|
|
149
149
|
const testObj = getTestAndMessage(title);
|
|
150
|
-
if (error && error.stack && test.steps && test.steps.length) {
|
|
151
|
-
error.stack = test.steps[test.steps.length - 1].line();
|
|
152
|
-
}
|
|
153
150
|
|
|
154
151
|
const files = [];
|
|
155
152
|
if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
const { Formatter, formatterHelpers } = require('@cucumber/cucumber');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const { STATUS,
|
|
5
|
+
const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../../constants');
|
|
6
6
|
const TestomatClient = require('../../client');
|
|
7
|
-
const logger = require('../../logger');
|
|
7
|
+
const logger = require('../../storages/logger');
|
|
8
8
|
const { parseTest, fileSystem } = require('../../utils/utils');
|
|
9
9
|
|
|
10
10
|
const { GherkinDocumentParser, PickleParser } = formatterHelpers;
|
|
@@ -37,7 +37,7 @@ class CucumberReporter extends Formatter {
|
|
|
37
37
|
|
|
38
38
|
parseEnvelope(envelope) {
|
|
39
39
|
if (envelope.testRunStarted) {
|
|
40
|
-
fileSystem.clearDir(
|
|
40
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
41
41
|
}
|
|
42
42
|
if (envelope.testCaseStarted && this.client) {
|
|
43
43
|
this.client.createRun();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const { Formatter } = require('cucumber');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { parseTest, fileSystem } = require('../../utils/utils');
|
|
5
|
-
const { STATUS,
|
|
5
|
+
const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../../constants');
|
|
6
6
|
const TestomatClient = require('../../client');
|
|
7
7
|
|
|
8
8
|
const createTestomatFormatter = apiKey => {
|
|
@@ -97,7 +97,7 @@ const createTestomatFormatter = apiKey => {
|
|
|
97
97
|
|
|
98
98
|
options.eventBroadcaster.on('gherkin-document', addDocument);
|
|
99
99
|
options.eventBroadcaster.on('test-run-started', () => {
|
|
100
|
-
fileSystem.clearDir(
|
|
100
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
101
101
|
this?.client?.createRun();
|
|
102
102
|
});
|
|
103
103
|
options.eventBroadcaster.on('test-case-finished', this.onTestCaseFinished.bind(this));
|
package/lib/adapter/jest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const TestomatClient = require('../client');
|
|
2
|
-
const { STATUS,
|
|
2
|
+
const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
3
3
|
const { parseTest, ansiRegExp, fileSystem } = require('../utils/utils');
|
|
4
4
|
|
|
5
5
|
class JestReporter {
|
|
@@ -11,20 +11,9 @@ class JestReporter {
|
|
|
11
11
|
this.client.createRun();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
static getIdOfCurrentlyRunningTest() {
|
|
15
|
-
if (!process.env.JEST_WORKER_ID) return null;
|
|
16
|
-
try {
|
|
17
|
-
// @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
|
|
18
|
-
// eslint-disable-next-line no-undef
|
|
19
|
-
if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
|
|
20
|
-
} catch (e) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
14
|
onRunStart() {
|
|
26
15
|
// clear tmp dir
|
|
27
|
-
fileSystem.clearDir(
|
|
16
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
28
17
|
}
|
|
29
18
|
|
|
30
19
|
onTestResult(test, testResult) {
|
|
@@ -72,9 +61,6 @@ class JestReporter {
|
|
|
72
61
|
const { numFailedTests } = results;
|
|
73
62
|
const status = numFailedTests === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
74
63
|
this.client.updateRunStatus(status);
|
|
75
|
-
|
|
76
|
-
// clear tmp dir
|
|
77
|
-
fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
66
|
|
package/lib/adapter/mocha.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
2
2
|
const Mocha = require('mocha');
|
|
3
|
-
const debug = require('debug')('@testomatio/reporter:adapter:mocha');
|
|
4
3
|
const chalk = require('chalk');
|
|
5
4
|
const TestomatClient = require('../client');
|
|
6
|
-
const { STATUS,
|
|
7
|
-
const { parseTest,
|
|
8
|
-
const ArtifactStorage = require('../_ArtifactStorageOld');
|
|
5
|
+
const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
6
|
+
const { parseTest, fileSystem } = require('../utils/utils');
|
|
9
7
|
|
|
10
8
|
const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } =
|
|
11
9
|
Mocha.Runner.constants;
|
|
@@ -15,7 +13,7 @@ function MochaReporter(runner, opts) {
|
|
|
15
13
|
let passes = 0;
|
|
16
14
|
let failures = 0;
|
|
17
15
|
let skipped = 0;
|
|
18
|
-
let artifactStore;
|
|
16
|
+
// let artifactStore;
|
|
19
17
|
|
|
20
18
|
const apiKey = opts?.reporterOptions?.apiKey || process.env.TESTOMATIO;
|
|
21
19
|
|
|
@@ -24,13 +22,7 @@ function MochaReporter(runner, opts) {
|
|
|
24
22
|
runner.on(EVENT_RUN_BEGIN, () => {
|
|
25
23
|
client.createRun();
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
toFile: true,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
artifactStore = runner._workerReporter !== undefined ? new ArtifactStorage(params) : new ArtifactStorage();
|
|
32
|
-
|
|
33
|
-
fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
|
|
25
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
34
26
|
});
|
|
35
27
|
|
|
36
28
|
runner.on(EVENT_TEST_PASS, async test => {
|
|
@@ -38,20 +30,11 @@ function MochaReporter(runner, opts) {
|
|
|
38
30
|
console.log(chalk.bold.green('✔'), test.fullTitle());
|
|
39
31
|
const testId = parseTest(test.title);
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
client.addTestRun(
|
|
47
|
-
STATUS.PASSED,
|
|
48
|
-
{
|
|
49
|
-
test_id: testId,
|
|
50
|
-
title: test.title,
|
|
51
|
-
time: test.duration,
|
|
52
|
-
},
|
|
53
|
-
content,
|
|
54
|
-
);
|
|
33
|
+
client.addTestRun(STATUS.PASSED, {
|
|
34
|
+
test_id: testId,
|
|
35
|
+
title: test.title,
|
|
36
|
+
time: test.duration,
|
|
37
|
+
});
|
|
55
38
|
});
|
|
56
39
|
|
|
57
40
|
runner.on(EVENT_TEST_PENDING, test => {
|
|
@@ -70,29 +53,18 @@ function MochaReporter(runner, opts) {
|
|
|
70
53
|
console.log(chalk.bold.red('✖'), test.fullTitle(), chalk.gray(err.message));
|
|
71
54
|
const testId = parseTest(test.title);
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
STATUS.FAILED,
|
|
80
|
-
{
|
|
81
|
-
error: err,
|
|
82
|
-
test_id: testId,
|
|
83
|
-
title: test.title,
|
|
84
|
-
time: test.duration,
|
|
85
|
-
},
|
|
86
|
-
content,
|
|
87
|
-
);
|
|
56
|
+
client.addTestRun(STATUS.FAILED, {
|
|
57
|
+
error: err,
|
|
58
|
+
test_id: testId,
|
|
59
|
+
title: test.title,
|
|
60
|
+
time: test.duration,
|
|
61
|
+
});
|
|
88
62
|
});
|
|
89
63
|
|
|
90
64
|
runner.on(EVENT_RUN_END, () => {
|
|
91
65
|
const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
92
66
|
console.log(chalk.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
|
|
93
67
|
client.updateRunStatus(status);
|
|
94
|
-
|
|
95
|
-
artifactStore.cleanup();
|
|
96
68
|
});
|
|
97
69
|
}
|
|
98
70
|
|
|
@@ -3,7 +3,7 @@ const crypto = require('crypto');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const { APP_PREFIX, STATUS: Status,
|
|
6
|
+
const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
7
7
|
const TestomatioClient = require('../client');
|
|
8
8
|
const { isArtifactsEnabled } = require('../fileUploader');
|
|
9
9
|
const { parseTest, fileSystem } = require('../utils/utils');
|
|
@@ -19,15 +19,17 @@ class PlaywrightReporter {
|
|
|
19
19
|
|
|
20
20
|
onBegin(_config, suite) {
|
|
21
21
|
// clean data storage
|
|
22
|
-
fileSystem.clearDir(
|
|
22
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
23
23
|
if (!this.client) return;
|
|
24
24
|
this.suite = suite;
|
|
25
25
|
this.client.createRun();
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/*
|
|
29
|
+
onTestBegin(test) {
|
|
30
|
+
// does not work; value is not to the storage context
|
|
31
|
+
global.testTitle = test.title;
|
|
32
|
+
} */
|
|
31
33
|
|
|
32
34
|
onTestEnd(test, result) {
|
|
33
35
|
if (!this.client) return;
|
|
@@ -44,23 +46,33 @@ class PlaywrightReporter {
|
|
|
44
46
|
appendStep(step, steps);
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
let logs = '';
|
|
50
|
+
if (result.stderr.length || result.stdout.length) {
|
|
51
|
+
logs = `\n\n${chalk.bold('Logs:')}\n${chalk.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reportTestPromise = this.client
|
|
55
|
+
.addTestRun(checkStatus(result.status), {
|
|
56
|
+
error,
|
|
57
|
+
test_id: testId,
|
|
58
|
+
suite_title,
|
|
59
|
+
title,
|
|
60
|
+
steps: steps.join('\n'),
|
|
61
|
+
time: duration,
|
|
62
|
+
stack: logs,
|
|
63
|
+
})
|
|
64
|
+
.then(pipes => {
|
|
65
|
+
testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
|
|
66
|
+
|
|
67
|
+
this.uploads.push({
|
|
68
|
+
testId,
|
|
69
|
+
title,
|
|
70
|
+
suite_title,
|
|
71
|
+
files: result.attachments.filter(a => a.body || a.path),
|
|
72
|
+
});
|
|
73
|
+
// remove empty uploads
|
|
74
|
+
this.uploads = this.uploads.filter(upload => upload.files.length);
|
|
62
75
|
});
|
|
63
|
-
});
|
|
64
76
|
|
|
65
77
|
reportTestPromises.push(reportTestPromise);
|
|
66
78
|
}
|
package/lib/bin/startTest.js
CHANGED
|
@@ -53,6 +53,12 @@ program
|
|
|
53
53
|
|
|
54
54
|
let exitCode = 0;
|
|
55
55
|
|
|
56
|
+
if (!command.split) {
|
|
57
|
+
process.exitCode = 255;
|
|
58
|
+
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
57
63
|
|
|
58
64
|
if(filter) {
|
|
@@ -74,12 +80,6 @@ program
|
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
if (!command.split) {
|
|
78
|
-
process.exitCode = 255;
|
|
79
|
-
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
83
|
const testCmds = command.split(' ');
|
|
84
84
|
console.log(APP_PREFIX, `🚀 Running`, chalk.green(command));
|
|
85
85
|
|
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@ const { randomUUID } = require('crypto');
|
|
|
8
8
|
const upload = require('./fileUploader');
|
|
9
9
|
const { APP_PREFIX } = require('./constants');
|
|
10
10
|
const pipesFactory = require('./pipe');
|
|
11
|
+
const artifactStorage = require('./storages/artifact-storage');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @typedef {import('../types').TestData} TestData
|
|
@@ -96,9 +97,6 @@ class Client {
|
|
|
96
97
|
// all pipes disabled, skipping
|
|
97
98
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
98
99
|
|
|
99
|
-
global.testomatioArtifacts = [];
|
|
100
|
-
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
101
|
-
|
|
102
100
|
this.queue = this.queue
|
|
103
101
|
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
104
102
|
.catch(err => console.log(APP_PREFIX, err))
|
|
@@ -112,11 +110,9 @@ class Client {
|
|
|
112
110
|
*
|
|
113
111
|
* @param {string|undefined} status
|
|
114
112
|
* @param {TestData} [testData]
|
|
115
|
-
* @param {string[]} [storeArtifacts]
|
|
116
113
|
* @returns {Promise<PipeResult[]>}
|
|
117
114
|
*/
|
|
118
|
-
async addTestRun(status, testData
|
|
119
|
-
debug('Adding test run for test', testData?.test_id || 'unknown test');
|
|
115
|
+
async addTestRun(status, testData) {
|
|
120
116
|
// all pipes disabled, skipping
|
|
121
117
|
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
122
118
|
|
|
@@ -155,23 +151,21 @@ class Client {
|
|
|
155
151
|
|
|
156
152
|
stack += testData.stack || '';
|
|
157
153
|
|
|
154
|
+
// ATTACH LOGS from storage
|
|
158
155
|
// in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
|
|
159
|
-
const logger = require('./logger');
|
|
156
|
+
const logger = require('./storages/logger');
|
|
160
157
|
const testLogs = logger.getLogs(test_id);
|
|
161
158
|
// debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
162
159
|
if (stack) stack += '\n\n';
|
|
163
160
|
stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
|
|
164
161
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
162
|
+
// GET ARTIFACTS from storage
|
|
163
|
+
const artifactFiles = artifactStorage.get(test_id);
|
|
164
|
+
if (artifactFiles) files.push(...artifactFiles);
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
global.testomatioArtifacts = [];
|
|
174
|
-
}
|
|
166
|
+
// GET KEY-VALUEs from storage
|
|
167
|
+
const keyValueStorage = require('./storages/key-value-storage');
|
|
168
|
+
const keyValues = keyValueStorage.get(test_id);
|
|
175
169
|
|
|
176
170
|
for (const file of files) {
|
|
177
171
|
uploadedFiles.push(upload.uploadFileByPath(file, this.uuid));
|
|
@@ -184,7 +178,7 @@ class Client {
|
|
|
184
178
|
|
|
185
179
|
const artifacts = await Promise.all(uploadedFiles);
|
|
186
180
|
|
|
187
|
-
global.testomatioArtifacts = [];
|
|
181
|
+
// global.testomatioArtifacts = [];
|
|
188
182
|
|
|
189
183
|
this.totalUploaded += uploadedFiles.filter(n => n).length;
|
|
190
184
|
|
|
@@ -202,6 +196,7 @@ class Client {
|
|
|
202
196
|
message,
|
|
203
197
|
run_time: parseFloat(time),
|
|
204
198
|
artifacts,
|
|
199
|
+
meta: keyValues,
|
|
205
200
|
};
|
|
206
201
|
|
|
207
202
|
this.queue = this.queue.then(() =>
|
|
@@ -237,14 +232,16 @@ class Client {
|
|
|
237
232
|
this.queue = this.queue
|
|
238
233
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
239
234
|
.then(() => {
|
|
240
|
-
debug('TOTAL
|
|
235
|
+
debug('TOTAL artifacts', this.totalUploaded);
|
|
236
|
+
if (this.totalUploaded && !upload.isArtifactsEnabled())
|
|
237
|
+
debug(`${this.totalUploaded} artifacts are not uploaded, because artifacts uploading is not enabled`);
|
|
241
238
|
|
|
242
|
-
if (
|
|
239
|
+
if (this.totalUploaded && upload.isArtifactsEnabled()) {
|
|
243
240
|
console.log(
|
|
244
241
|
APP_PREFIX,
|
|
245
|
-
`🗄️
|
|
242
|
+
`🗄️ ${this.totalUploaded} artifacts ${
|
|
246
243
|
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
247
|
-
} uploaded to S3 bucket
|
|
244
|
+
} uploaded to S3 bucket`,
|
|
248
245
|
);
|
|
249
246
|
}
|
|
250
247
|
})
|
package/lib/constants.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
|
|
3
5
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
4
|
-
const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
mainDir: "testomatio_tmp",
|
|
8
|
-
}
|
|
7
|
+
const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
|
|
9
8
|
|
|
10
9
|
const CSV_HEADERS = [
|
|
11
10
|
{ id: 'suite_title', title: 'Suite_title' },
|
|
@@ -24,8 +23,7 @@ const STATUS = {
|
|
|
24
23
|
|
|
25
24
|
module.exports = {
|
|
26
25
|
APP_PREFIX,
|
|
27
|
-
|
|
28
|
-
TESTOMAT_TMP_STORAGE,
|
|
26
|
+
TESTOMAT_TMP_STORAGE_DIR,
|
|
29
27
|
CSV_HEADERS,
|
|
30
28
|
STATUS,
|
|
31
29
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const logger = require('./storages/logger');
|
|
2
|
+
const artifactStorage = require('./storages/artifact-storage');
|
|
3
|
+
const keyValueStorage = require('./storages/key-value-storage');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stores path to file as artifact and uploads it to the S3 storage
|
|
7
|
+
* @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
|
|
8
|
+
*/
|
|
9
|
+
function saveArtifact(data, context = null) {
|
|
10
|
+
if (!data) return;
|
|
11
|
+
artifactStorage.put(data, context);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attach log message(s) to the test report
|
|
16
|
+
* @param {...any} args
|
|
17
|
+
*/
|
|
18
|
+
function logMessage(...args) {
|
|
19
|
+
logger.log(...args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Similar to "log" function but marks message in report as a step
|
|
24
|
+
* @param {*} message
|
|
25
|
+
*/
|
|
26
|
+
function addStep(message) {
|
|
27
|
+
logger.step(message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add key-value pair(s) to the test report
|
|
32
|
+
* @param {*} keyValue
|
|
33
|
+
*/
|
|
34
|
+
function setKeyValue(keyValue) {
|
|
35
|
+
keyValueStorage.put(keyValue);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
artifact: saveArtifact,
|
|
40
|
+
log: logMessage,
|
|
41
|
+
step: addStep,
|
|
42
|
+
keyValue: setKeyValue,
|
|
43
|
+
};
|
package/lib/reporter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const logger = require('./logger');
|
|
1
|
+
const logger = require('./storages/logger');
|
|
2
2
|
const TestomatClient = require('./client');
|
|
3
3
|
const TRConstants = require('./constants');
|
|
4
|
-
const TRArtifacts = require('./_ArtifactStorageOld');
|
|
5
4
|
|
|
6
5
|
const log = logger.templateLiteralLog.bind(logger);
|
|
7
6
|
const step = logger.step.bind(logger);
|
|
7
|
+
const reporterFunctions = require('./reporter-functions');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
logger,
|
|
@@ -13,6 +13,11 @@ module.exports = {
|
|
|
13
13
|
step,
|
|
14
14
|
TestomatClient,
|
|
15
15
|
TRConstants,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports.testomat = {
|
|
19
|
+
artifact: reporterFunctions.artifact,
|
|
20
|
+
log: reporterFunctions.log,
|
|
21
|
+
step: reporterFunctions.step,
|
|
22
|
+
meta: reporterFunctions.keyValue,
|
|
18
23
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:artifact-storage');
|
|
2
|
+
const DataStorage = require('./data-storage');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Artifact storage is supposed to store file paths
|
|
6
|
+
*/
|
|
7
|
+
class ArtifactStorage {
|
|
8
|
+
|
|
9
|
+
// there is autocompletion for the class methods if implemented singleton this way
|
|
10
|
+
constructor() {
|
|
11
|
+
this.dataStorage = new DataStorage('artifact');
|
|
12
|
+
|
|
13
|
+
// singleton
|
|
14
|
+
if (!ArtifactStorage.instance) {
|
|
15
|
+
ArtifactStorage.instance = this;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// singleton
|
|
20
|
+
static getInstance() {
|
|
21
|
+
if (!ArtifactStorage.instance) {
|
|
22
|
+
ArtifactStorage.instance = new ArtifactStorage();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ArtifactStorage.instance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stores path to file as artifact and uploads it to the S3 storage
|
|
30
|
+
* @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
|
|
31
|
+
* @param {*} context testId or test title
|
|
32
|
+
*/
|
|
33
|
+
put(data, context = null) {
|
|
34
|
+
if (!data) return;
|
|
35
|
+
debug('Save artifact:', data);
|
|
36
|
+
this.dataStorage.putData(data, context);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns list of artifacts to upload
|
|
41
|
+
* @param {*} context testId or test context from test runner
|
|
42
|
+
* @returns {string | {path: string, type: string, name: string}[]}
|
|
43
|
+
*/
|
|
44
|
+
get(context) {
|
|
45
|
+
if (!context) return null;
|
|
46
|
+
|
|
47
|
+
// array of any data
|
|
48
|
+
let artifacts = this.dataStorage.getData(context);
|
|
49
|
+
if (!artifacts || !artifacts.length) return null;
|
|
50
|
+
|
|
51
|
+
artifacts = artifacts.map(artifactData => {
|
|
52
|
+
// artifact could be an object ({type, path, name} props) or string
|
|
53
|
+
let artifact;
|
|
54
|
+
try {
|
|
55
|
+
artifact = JSON.parse(artifactData);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
artifact = artifactData;
|
|
58
|
+
}
|
|
59
|
+
return artifact;
|
|
60
|
+
});
|
|
61
|
+
artifacts = artifacts.filter(artifact => !!artifact);
|
|
62
|
+
return artifacts.length ? artifacts : null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ArtifactStorage.instance = null;
|
|
67
|
+
|
|
68
|
+
// const artifactStorage = ArtifactStorage.getInstance();
|
|
69
|
+
const artifactStorage = new ArtifactStorage();
|
|
70
|
+
module.exports = artifactStorage;
|
|
@@ -3,16 +3,18 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { join } = require('path');
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
const getTestIdFromTestTitle =
|
|
6
|
+
const { TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
7
|
+
const { fileSystem, jestHelpers, parseTest } = require('../utils/utils');
|
|
8
|
+
|
|
9
|
+
const getTestIdFromTestTitle = parseTest;
|
|
10
10
|
|
|
11
11
|
class DataStorage {
|
|
12
12
|
/**
|
|
13
13
|
* Creates data storage instance for specific data type.
|
|
14
14
|
* Stores data to global variable or to file depending on what is applicable for current test runner
|
|
15
15
|
* (running environment).
|
|
16
|
+
* Recommend to use composition while using this class (instead of inheritance).
|
|
17
|
+
* ! Also the class which will use data storage should be singleton (to avoid data loss).
|
|
16
18
|
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
17
19
|
*/
|
|
18
20
|
constructor(dataType) {
|
|
@@ -20,8 +22,35 @@ class DataStorage {
|
|
|
20
22
|
this.isFileStorage = true;
|
|
21
23
|
this.#refreshStorageType();
|
|
22
24
|
|
|
23
|
-
this.dataDirPath = path.join(
|
|
24
|
-
|
|
25
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, this.dataType);
|
|
26
|
+
|
|
27
|
+
// set test title for mocha (could not be moved to adapter)
|
|
28
|
+
// it does not override existing hook, just add new one
|
|
29
|
+
try {
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
if (!beforeEach) return;
|
|
32
|
+
// @ts-ignore-next-line
|
|
33
|
+
beforeEach(function () {
|
|
34
|
+
if (this.currentTest?.__mocha_id__) {
|
|
35
|
+
global.testTitle = this.currentTest.fullTitle();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// set test title for playwright
|
|
43
|
+
try {
|
|
44
|
+
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
|
|
45
|
+
const { test } = require('@playwright/test');
|
|
46
|
+
// eslint-disable-next-line no-empty-pattern
|
|
47
|
+
test.beforeEach(async ({}, testInfo) => {
|
|
48
|
+
global.testTitle = testInfo.title;
|
|
49
|
+
global.testomatioRunningEnvironment = 'playwright';
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
/**
|
|
@@ -44,15 +73,18 @@ class DataStorage {
|
|
|
44
73
|
}
|
|
45
74
|
|
|
46
75
|
/**
|
|
47
|
-
* Puts any data to storage (file or global variable)
|
|
48
|
-
*
|
|
76
|
+
* Puts any data to storage (file or global variable).
|
|
77
|
+
* If file: stores data as text, if global variable – stores as array of data.
|
|
78
|
+
* @param {*} data anything you want to store (string, object, array, etc)
|
|
49
79
|
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
50
80
|
* @returns
|
|
51
81
|
*/
|
|
52
82
|
putData(data, context = null) {
|
|
53
|
-
this
|
|
83
|
+
fileSystem.createDir(this.dataDirPath);
|
|
54
84
|
|
|
55
|
-
|
|
85
|
+
this.#refreshStorageType();
|
|
86
|
+
context = context ?? global.testTitle ?? null;
|
|
87
|
+
let testId = this.#tryToRetrieveTestId(context) || context || null;
|
|
56
88
|
|
|
57
89
|
if (this.runningEnvironment === 'codeceptjs') {
|
|
58
90
|
this.isFileStorage = false;
|
|
@@ -60,7 +92,7 @@ class DataStorage {
|
|
|
60
92
|
}
|
|
61
93
|
|
|
62
94
|
if (this.runningEnvironment === 'jest') {
|
|
63
|
-
testId = testId ??
|
|
95
|
+
testId = testId ?? jestHelpers.getIdOfCurrentlyRunningTest();
|
|
64
96
|
}
|
|
65
97
|
|
|
66
98
|
// logs in playwright are gathered by pw framework itself
|
|
@@ -74,7 +106,7 @@ class DataStorage {
|
|
|
74
106
|
testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
75
107
|
|
|
76
108
|
if (!testId) {
|
|
77
|
-
debug(`No test id provided for ${this.dataType} data
|
|
109
|
+
debug(`No test id provided for "${this.dataType}" data:`, data);
|
|
78
110
|
return;
|
|
79
111
|
}
|
|
80
112
|
|
|
@@ -87,12 +119,10 @@ class DataStorage {
|
|
|
87
119
|
|
|
88
120
|
/**
|
|
89
121
|
* Returns data, stored for specific testId (or data which was stored without test id specified).
|
|
90
|
-
* This method will get data from global variable
|
|
91
|
-
* Thus, good approach is to remove file storage folder before each test run (and after, for sure).
|
|
122
|
+
* This method will get data from global variable and/or from from file (previosly saved with put method).
|
|
92
123
|
*
|
|
93
|
-
*
|
|
94
|
-
* @
|
|
95
|
-
* @returns
|
|
124
|
+
* @param {*} context could be testId or any context (test title, suite, etc) which is used to define testId
|
|
125
|
+
* @returns array of data (any type), null (if no data found for test) or string (if data type is log)
|
|
96
126
|
*/
|
|
97
127
|
getData(context) {
|
|
98
128
|
this.#refreshStorageType();
|
|
@@ -100,9 +130,6 @@ class DataStorage {
|
|
|
100
130
|
let testId = null;
|
|
101
131
|
if (typeof context === 'string') {
|
|
102
132
|
testId = this.#tryToRetrieveTestId(context) || context;
|
|
103
|
-
} else {
|
|
104
|
-
// TODO: derive testId from context
|
|
105
|
-
// testId = context...
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
if (!testId) {
|
|
@@ -110,12 +137,20 @@ class DataStorage {
|
|
|
110
137
|
return null;
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
let
|
|
140
|
+
let testDataFromFile = [];
|
|
141
|
+
let testDataFromGlobalVar = [];
|
|
114
142
|
|
|
115
143
|
if (global?.testomatioDataStore) {
|
|
116
|
-
|
|
144
|
+
testDataFromGlobalVar = this.#getDataFromGlobalVar(testId);
|
|
117
145
|
// these frameworks use global variable storage
|
|
118
|
-
if (
|
|
146
|
+
// if (testDataFromGlobalVar && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) {
|
|
147
|
+
if (testDataFromGlobalVar) {
|
|
148
|
+
// return as string if its log data
|
|
149
|
+
if (this.dataType === 'log' && testDataFromGlobalVar.length) return testDataFromGlobalVar.join('\n');
|
|
150
|
+
// return as array for other data types
|
|
151
|
+
if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
|
|
152
|
+
}
|
|
153
|
+
// don't return nothing if no data in global variable
|
|
119
154
|
}
|
|
120
155
|
|
|
121
156
|
/* condition is removed for mocha
|
|
@@ -126,11 +161,16 @@ class DataStorage {
|
|
|
126
161
|
// if (testData) return testData;
|
|
127
162
|
// }
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
testData += testDataFromFile || '';
|
|
164
|
+
testDataFromFile = this.#getDataFromFile(testId);
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
if (testDataFromFile.length) {
|
|
167
|
+
if (this.dataType === 'log') return testDataFromFile.join('\n');
|
|
168
|
+
return testDataFromFile;
|
|
169
|
+
}
|
|
170
|
+
debug(`No "${this.dataType}" data for test id ${testId} in both file and global variable`);
|
|
171
|
+
|
|
172
|
+
// in case no data found for test
|
|
173
|
+
return null;
|
|
134
174
|
}
|
|
135
175
|
|
|
136
176
|
/**
|
|
@@ -166,46 +206,63 @@ class DataStorage {
|
|
|
166
206
|
this.runningEnvironment = this.getRunningEnviroment();
|
|
167
207
|
|
|
168
208
|
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
169
|
-
if (
|
|
209
|
+
if (this.runningEnvironment === 'codeceptjs') this.isFileStorage = false;
|
|
210
|
+
// playwrigth stores logs by itself; but file storage is used for other data types for playwright
|
|
211
|
+
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') this.isFileStorage = false;
|
|
170
212
|
}
|
|
171
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {*} testId
|
|
216
|
+
* @returns aray of data (any type)
|
|
217
|
+
*/
|
|
172
218
|
#getDataFromGlobalVar(testId) {
|
|
173
219
|
try {
|
|
174
220
|
if (global?.testomatioDataStore[this.dataType]) {
|
|
175
221
|
const testData = global.testomatioDataStore[this.dataType][testId];
|
|
176
|
-
debug(`
|
|
177
|
-
return testData ||
|
|
222
|
+
if (testData) debug(`"${this.dataType}" data for test id ${testId}:`, testData.join(', '));
|
|
223
|
+
return testData || [];
|
|
178
224
|
}
|
|
179
|
-
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
180
|
-
return
|
|
225
|
+
// debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
226
|
+
return [];
|
|
181
227
|
} catch (e) {
|
|
182
228
|
// there could be no data, ignore
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
|
|
232
|
+
/**
|
|
233
|
+
*
|
|
234
|
+
* @param {*} testId
|
|
235
|
+
* @returns array of data (any type)
|
|
236
|
+
*/
|
|
186
237
|
#getDataFromFile(testId) {
|
|
187
238
|
try {
|
|
188
239
|
const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
|
|
189
240
|
if (fs.existsSync(filepath)) {
|
|
190
|
-
const
|
|
191
|
-
debug(`
|
|
192
|
-
|
|
241
|
+
const testDataAsText = fs.readFileSync(filepath, 'utf-8');
|
|
242
|
+
if (testDataAsText) debug(`"${this.dataType}" data for test id ${testId}:`, testDataAsText);
|
|
243
|
+
const testDataArr = testDataAsText?.split(os.EOL) || [];
|
|
244
|
+
return testDataArr;
|
|
193
245
|
}
|
|
194
|
-
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
195
|
-
return
|
|
246
|
+
// debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
247
|
+
return [];
|
|
196
248
|
} catch (e) {
|
|
197
249
|
// there could be no data, ignore
|
|
198
250
|
}
|
|
199
|
-
return
|
|
251
|
+
return [];
|
|
200
252
|
}
|
|
201
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
256
|
+
* @param {*} data
|
|
257
|
+
* @param {*} testId
|
|
258
|
+
*/
|
|
202
259
|
#putDataToGlobalVar(data, testId) {
|
|
203
|
-
debug(
|
|
260
|
+
debug(`Saving data to global variable for test ${testId}:`, data);
|
|
204
261
|
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
205
262
|
if (!global.testomatioDataStore?.[this.dataType]) global.testomatioDataStore[this.dataType] = {};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
263
|
+
|
|
264
|
+
if (!global.testomatioDataStore?.[this.dataType][testId]) global.testomatioDataStore[this.dataType][testId] = [];
|
|
265
|
+
global.testomatioDataStore[this.dataType][testId].push(data);
|
|
209
266
|
}
|
|
210
267
|
|
|
211
268
|
#putDataToFile(data, testId) {
|
|
@@ -215,18 +272,36 @@ class DataStorage {
|
|
|
215
272
|
if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
|
|
216
273
|
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
217
274
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
275
|
+
// append new line if file already exists (in this case its definitely includes some data)
|
|
276
|
+
if (fs.existsSync(filepath)) {
|
|
277
|
+
fs.appendFileSync(filepath, os.EOL + data, 'utf-8');
|
|
278
|
+
} else {
|
|
279
|
+
fs.writeFileSync(filepath, data, 'utf-8');
|
|
280
|
+
}
|
|
221
281
|
}
|
|
222
282
|
}
|
|
223
283
|
|
|
224
|
-
module.exports
|
|
284
|
+
module.exports = DataStorage;
|
|
225
285
|
|
|
226
286
|
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
227
287
|
// prevent blocking and improve performance (probably queue usage will be required)
|
|
228
288
|
|
|
229
289
|
// TODO: rewrite client - everything regarding storing artifacts
|
|
230
290
|
// TODO: try to define adapter inside client
|
|
231
|
-
// TODO: use .env
|
|
232
291
|
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
292
|
+
|
|
293
|
+
/* does not work for Playwright
|
|
294
|
+
try {
|
|
295
|
+
const { test } = require('@playwright/test');
|
|
296
|
+
test.beforeEach(async ({}, testInfo) => {
|
|
297
|
+
global.testTitle = testInfo.title;
|
|
298
|
+
global.testomatioRunningEnvironment = 'playwright';
|
|
299
|
+
});
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// ignore
|
|
302
|
+
}
|
|
303
|
+
// Playwright storage works fine only when running tests from a single file;
|
|
304
|
+
// otherwise it is not possible to define test id/title, it is overwritten
|
|
305
|
+
TODO: the only way implementation for Pw for now is to pass testInfo inside test
|
|
306
|
+
// (and process this testInfo inside "put" function)
|
|
307
|
+
*/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:key-value-storage');
|
|
2
|
+
const DataStorage = require('./data-storage');
|
|
3
|
+
|
|
4
|
+
class KeyValueStorage {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.dataStorage = new DataStorage('keyvalue');
|
|
7
|
+
|
|
8
|
+
// singleton
|
|
9
|
+
if (!KeyValueStorage.instance) {
|
|
10
|
+
KeyValueStorage.instance = this;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stores key-value pair and passes it to reporter
|
|
16
|
+
* @param {{key: string}} keyValue - key-value pair(s) as object
|
|
17
|
+
* @param {*} context testId or test title
|
|
18
|
+
*/
|
|
19
|
+
put(keyValue, context = null) {
|
|
20
|
+
if (!keyValue) return;
|
|
21
|
+
this.dataStorage.putData(keyValue, context);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#isKeyValueObject(smth) {
|
|
25
|
+
return smth && typeof smth === 'object' && !Array.isArray(smth) && smth !== null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns key-values pairs for the test as object
|
|
30
|
+
* @param {*} context testId or test context from test runner
|
|
31
|
+
* @returns {Object} key-values pairs as object, e.g. {priority: 'high', browser: 'chrome'}
|
|
32
|
+
*/
|
|
33
|
+
get(context) {
|
|
34
|
+
if (!context) return null;
|
|
35
|
+
|
|
36
|
+
const keyValuesList = this.dataStorage.getData(context);
|
|
37
|
+
if (!keyValuesList || !keyValuesList?.length) return null;
|
|
38
|
+
|
|
39
|
+
const keyValues = {};
|
|
40
|
+
for (const keyValue of keyValuesList) {
|
|
41
|
+
if (this.#isKeyValueObject(keyValue)) {
|
|
42
|
+
Object.assign(keyValues, keyValue);
|
|
43
|
+
} else if (typeof keyValue === 'string') {
|
|
44
|
+
try {
|
|
45
|
+
Object.assign(keyValues, JSON.parse(keyValue));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
debug(`Error parsing key-values for test ${context}`, keyValue);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Object.keys(keyValues).length ? keyValues : null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
KeyValueStorage.instance = null;
|
|
57
|
+
|
|
58
|
+
module.exports = new KeyValueStorage();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const debug = require('debug')('@testomatio/reporter:logger');
|
|
3
|
-
const
|
|
3
|
+
const DataStorage = require('./data-storage');
|
|
4
4
|
|
|
5
5
|
const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
|
|
6
6
|
const LEVELS = {
|
|
@@ -38,20 +38,6 @@ class Logger {
|
|
|
38
38
|
if (!Logger.instance) {
|
|
39
39
|
Logger.instance = this;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
// add beforeEach hook for mocha. it does not override existing hook, just add new one
|
|
43
|
-
try {
|
|
44
|
-
// @ts-ignore
|
|
45
|
-
if (!beforeEach) return;
|
|
46
|
-
// @ts-ignore-next-line
|
|
47
|
-
beforeEach(function () {
|
|
48
|
-
if (this.currentTest?.__mocha_id__) {
|
|
49
|
-
global.testTitle = this.currentTest.fullTitle();
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
} catch (e) {
|
|
53
|
-
// ignore
|
|
54
|
-
}
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
/**
|
|
@@ -61,9 +47,6 @@ class Logger {
|
|
|
61
47
|
* @param {...any} values
|
|
62
48
|
*/
|
|
63
49
|
step(strings, ...values) {
|
|
64
|
-
// get testId for mocha
|
|
65
|
-
const context = global.testTitle ?? null;
|
|
66
|
-
|
|
67
50
|
let logs = '';
|
|
68
51
|
for (let i = 0; i < strings.length; i++) {
|
|
69
52
|
logs += strings[i];
|
|
@@ -72,7 +55,7 @@ class Logger {
|
|
|
72
55
|
}
|
|
73
56
|
}
|
|
74
57
|
logs = chalk.blue(`> ${logs}`);
|
|
75
|
-
this.#dataStorage.putData(logs
|
|
58
|
+
this.#dataStorage.putData(logs);
|
|
76
59
|
}
|
|
77
60
|
|
|
78
61
|
/**
|
|
@@ -117,11 +100,6 @@ class Logger {
|
|
|
117
100
|
templateLiteralLog(strings, ...args) {
|
|
118
101
|
if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
|
|
119
102
|
if (Array.isArray(args)) args = args.filter(item => item !== '');
|
|
120
|
-
// entity which is used to define testId
|
|
121
|
-
let context = null;
|
|
122
|
-
|
|
123
|
-
// get testId for mocha
|
|
124
|
-
context = global.testTitle ?? null;
|
|
125
103
|
|
|
126
104
|
let logs;
|
|
127
105
|
// this block means tagged template is used (syntax like $`text ${someVar}`)
|
|
@@ -146,7 +124,7 @@ class Logger {
|
|
|
146
124
|
logs = this.#stringifyLogs(strings, ...args);
|
|
147
125
|
}
|
|
148
126
|
this.#originalUserLogger.log(logs);
|
|
149
|
-
this.#dataStorage.putData(logs
|
|
127
|
+
this.#dataStorage.putData(logs);
|
|
150
128
|
}
|
|
151
129
|
|
|
152
130
|
/**
|
|
@@ -158,18 +136,12 @@ class Logger {
|
|
|
158
136
|
#logWrapper(argsArray, level) {
|
|
159
137
|
if (!argsArray.length) return;
|
|
160
138
|
|
|
161
|
-
// entity which is used to define testId
|
|
162
|
-
let context = null;
|
|
163
|
-
|
|
164
|
-
// get context for mocha
|
|
165
|
-
context = global.testTitle ?? null;
|
|
166
|
-
|
|
167
139
|
const severity = LEVELS[level].severity;
|
|
168
140
|
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
169
141
|
|
|
170
142
|
const logs = this.#stringifyLogs(...argsArray);
|
|
171
143
|
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
172
|
-
this.#dataStorage.putData(colorizedLogs
|
|
144
|
+
this.#dataStorage.putData(colorizedLogs);
|
|
173
145
|
try {
|
|
174
146
|
// level.toLowerCase() represents method name (log, warn, error, etc)
|
|
175
147
|
this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
|
|
@@ -269,7 +241,6 @@ class Logger {
|
|
|
269
241
|
|
|
270
242
|
Logger.instance = null;
|
|
271
243
|
|
|
272
|
-
// module.exports.Logger = Logger;
|
|
273
244
|
const logger = new Logger();
|
|
274
245
|
|
|
275
246
|
module.exports = logger;
|
package/lib/utils/utils.js
CHANGED
|
@@ -290,6 +290,19 @@ function removeColorCodes(input) {
|
|
|
290
290
|
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
const jestHelpers = {
|
|
294
|
+
getIdOfCurrentlyRunningTest: () => {
|
|
295
|
+
if (!process.env.JEST_WORKER_ID) return null;
|
|
296
|
+
try {
|
|
297
|
+
// @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
|
|
298
|
+
// eslint-disable-next-line no-undef
|
|
299
|
+
if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
293
306
|
module.exports = {
|
|
294
307
|
isSameTest,
|
|
295
308
|
fetchSourceCode,
|
|
@@ -299,6 +312,7 @@ module.exports = {
|
|
|
299
312
|
fetchFilesFromStackTrace,
|
|
300
313
|
fileSystem,
|
|
301
314
|
getCurrentDateTime,
|
|
315
|
+
jestHelpers,
|
|
302
316
|
specificTestInfo,
|
|
303
317
|
isValidUrl,
|
|
304
318
|
ansiRegExp,
|
package/package.json
CHANGED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
const debug = require('debug')('@testomatio/reporter:storage');
|
|
2
|
-
const { join, resolve } = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const uuid = require('uuid');
|
|
6
|
-
const { TESTOMAT_ARTIFACT_SUFFIX } = require('./constants');
|
|
7
|
-
const { specificTestInfo } = require('./utils/utils');
|
|
8
|
-
|
|
9
|
-
class ArtifactStorage {
|
|
10
|
-
|
|
11
|
-
constructor(params) {
|
|
12
|
-
this.isFile = false;
|
|
13
|
-
this.isMemory = true;
|
|
14
|
-
this.storage = params?.toFile;
|
|
15
|
-
|
|
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)
|
|
21
|
-
this.isFile = true;
|
|
22
|
-
this.isMemory = false;
|
|
23
|
-
this.tmpDirFullpath = this.createTestomatTmpDir(ArtifactStorage._tmpPrefix);
|
|
24
|
-
debug('SAVE to tmp folder mode enabled!');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static async storeToFile(tmpDirName, artifact, testSuffix = "test") {
|
|
29
|
-
// this assumes to use multiple files for each test. do we really want this?
|
|
30
|
-
const suffix = TESTOMAT_ARTIFACT_SUFFIX + uuid.v4() + testSuffix;
|
|
31
|
-
const dirpath = join(os.tmpdir(), tmpDirName);
|
|
32
|
-
// why json?
|
|
33
|
-
const filepath = resolve(dirpath, `${suffix}.json`);
|
|
34
|
-
|
|
35
|
-
return fs.promises.appendFile(filepath, JSON.stringify(artifact));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
static async artifact(artifact, context) {
|
|
39
|
-
// TODO: mode for different pre-hooks. As variant - "all-tests" || "only_fail"
|
|
40
|
-
// const mode = process.env.TESTOMAT_ARTIFACTS || "only_fail";
|
|
41
|
-
|
|
42
|
-
// you save the artifact without specifying its source - test id
|
|
43
|
-
if (Array.isArray(global.testomatioArtifacts)) {
|
|
44
|
-
debug("Saving artifacts to global storage");
|
|
45
|
-
|
|
46
|
-
global.testomatioArtifacts.push(artifact);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (global?.testomatioArtifacts === undefined) {
|
|
50
|
-
const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
|
|
51
|
-
|
|
52
|
-
const testSuffix = specificTestInfo(context.test);
|
|
53
|
-
|
|
54
|
-
if (Array.isArray(tmpDirNames) && tmpDirNames.length) {
|
|
55
|
-
debug("Saving artifacts to memory tmp folder");
|
|
56
|
-
|
|
57
|
-
return ArtifactStorage.storeToFile(tmpDirNames[tmpDirNames.length - 1], artifact, testSuffix);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async artifactByTestName(test) {
|
|
63
|
-
const list = [];
|
|
64
|
-
|
|
65
|
-
if (this.isFile && this.tmpDirFullpath) {
|
|
66
|
-
const files = fs.readdirSync(this.tmpDirFullpath);
|
|
67
|
-
|
|
68
|
-
for (const file of files) {
|
|
69
|
-
if (file.includes(test)) {
|
|
70
|
-
const buff = await fs.promises.readFile(`${this.tmpDirFullpath }/${ file}`);
|
|
71
|
-
|
|
72
|
-
list.push(JSON.parse(buff.toString()));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return list;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// returns all the content from all files; do we really need it?
|
|
81
|
-
async tmpContents() {
|
|
82
|
-
const contents = [];
|
|
83
|
-
|
|
84
|
-
if (this.isFile && this.tmpDirFullpath) {
|
|
85
|
-
const files = fs.readdirSync(this.tmpDirFullpath);
|
|
86
|
-
|
|
87
|
-
for (const file of files) {
|
|
88
|
-
const buff = await fs.promises.readFile(`${this.tmpDirFullpath }/${ file}`);
|
|
89
|
-
const content = buff.toString();
|
|
90
|
-
|
|
91
|
-
contents.push(content);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return contents;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
createTestomatTmpDir(clientPrefix) {
|
|
99
|
-
return fs.mkdtempSync(join(os.tmpdir(), clientPrefix));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
clearTmpDirByName(name) {
|
|
103
|
-
const tmpDirPath = join(os.tmpdir(), name);
|
|
104
|
-
|
|
105
|
-
if (fs.existsSync(tmpDirPath)) {
|
|
106
|
-
fs.rmSync(tmpDirPath, { recursive: true });
|
|
107
|
-
debug(` Testomat tmpDir = ${tmpDirPath} was deleted successfully!`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// no need to use multiple dirs; we can implement it later if required
|
|
114
|
-
static tmpTestomatDirNames() {
|
|
115
|
-
const subname = ArtifactStorage._tmpPrefix || "tsmt_reporter";
|
|
116
|
-
|
|
117
|
-
return fs.readdirSync(os.tmpdir(), { withFileTypes: true })
|
|
118
|
-
.filter((item) => item.isDirectory())
|
|
119
|
-
.map((item) => item.name)
|
|
120
|
-
.filter((name) => name.includes(subname));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
cleanup() {
|
|
124
|
-
// when you do a cleanup, I know nothing about isFile param
|
|
125
|
-
if (this.isFile && this.tmpDirFullpath) {
|
|
126
|
-
const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
|
|
127
|
-
|
|
128
|
-
if (Array.isArray(tmpDirNames) && tmpDirNames.length) {
|
|
129
|
-
for (const name of tmpDirNames) {
|
|
130
|
-
this.clearTmpDirByName(name);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
debug("The tmp folder has not been created! Nothing to delete");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
ArtifactStorage._tmpPrefix = "tsmt_reporter";
|
|
141
|
-
|
|
142
|
-
module.exports = ArtifactStorage;
|
package/lib/artifactStorage.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// const debug = require('debug')('@testomatio/reporter:logger');
|
|
2
|
-
const { DataStorage } = require('./dataStorage');
|
|
3
|
-
|
|
4
|
-
class ArtifactStorage {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.dataStorage = new DataStorage('artifact');
|
|
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();
|