@testomatio/reporter 1.0.0-beta.4 → 1.0.1-6.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 +100 -2
- package/README.md +92 -540
- package/lib/{ArtifactStorage.js → _ArtifactStorageOld.js} +16 -5
- package/lib/adapter/codecept.js +83 -53
- package/lib/adapter/cucumber/current.js +95 -59
- package/lib/adapter/cucumber/legacy.js +25 -11
- package/lib/adapter/cypress-plugin/index.js +1 -1
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +30 -2
- package/lib/adapter/mocha.js +24 -24
- package/lib/adapter/playwright.js +18 -11
- package/lib/adapter/webdriver.js +1 -1
- package/lib/artifactStorage.js +25 -0
- package/lib/bin/reportXml.js +1 -0
- package/lib/bin/startTest.js +25 -4
- package/lib/client.js +117 -41
- package/lib/constants.js +5 -0
- package/lib/dataStorage.js +232 -0
- package/lib/junit-adapter/java.js +27 -9
- package/lib/logger.js +319 -0
- package/lib/pipe/csv.js +4 -1
- package/lib/pipe/github.js +26 -39
- package/lib/pipe/gitlab.js +5 -16
- package/lib/pipe/testomatio.js +138 -33
- package/lib/reporter.js +11 -2
- package/lib/utils/pipe_utils.js +135 -0
- package/lib/utils/utils.js +310 -0
- package/lib/xmlReader.js +132 -44
- package/package.json +9 -6
- package/lib/util.js +0 -184
|
@@ -0,0 +1,232 @@
|
|
|
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('./utils/utils');
|
|
9
|
+
const getTestIdFromTestTitle = require('./utils/utils').parseTest;
|
|
10
|
+
|
|
11
|
+
class DataStorage {
|
|
12
|
+
/**
|
|
13
|
+
* Creates data storage instance for specific data type.
|
|
14
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner
|
|
15
|
+
* (running environment).
|
|
16
|
+
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
17
|
+
*/
|
|
18
|
+
constructor(dataType) {
|
|
19
|
+
this.dataType = dataType || 'data';
|
|
20
|
+
this.isFileStorage = true;
|
|
21
|
+
this.#refreshStorageType();
|
|
22
|
+
|
|
23
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataType);
|
|
24
|
+
fileSystem.createDir(this.dataDirPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Try to define the running environment. Not 100% reliable and used as additional check
|
|
29
|
+
* @returns jest | mocha | ...
|
|
30
|
+
*/
|
|
31
|
+
getRunningEnviroment() {
|
|
32
|
+
// jest
|
|
33
|
+
if (process.env.JEST_WORKER_ID) return 'jest';
|
|
34
|
+
|
|
35
|
+
if (global.codeceptjs) return 'codeceptjs';
|
|
36
|
+
|
|
37
|
+
// 'cucumber:current', 'cucumber:legacy'
|
|
38
|
+
if (global.testomatioRunningEnvironment) return global.testomatioRunningEnvironment;
|
|
39
|
+
|
|
40
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL || process.env.TEST_WORKER_INDEX) return 'playwright';
|
|
41
|
+
|
|
42
|
+
// mocha - can't detect
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Puts any data to storage (file or global variable)
|
|
48
|
+
* @param {*} data anything you want to store
|
|
49
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
putData(data, context = null) {
|
|
53
|
+
this.#refreshStorageType();
|
|
54
|
+
|
|
55
|
+
let testId = this.#tryToRetrieveTestId(context) || null;
|
|
56
|
+
|
|
57
|
+
if (this.runningEnvironment === 'codeceptjs') {
|
|
58
|
+
this.isFileStorage = false;
|
|
59
|
+
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.runningEnvironment === 'jest') {
|
|
63
|
+
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// logs in playwright are gathered by pw framework itself
|
|
67
|
+
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
|
|
68
|
+
|
|
69
|
+
// get id from global store
|
|
70
|
+
if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
|
|
71
|
+
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
72
|
+
|
|
73
|
+
if (!testId && global?.currentlyRunningTestTitle)
|
|
74
|
+
testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
75
|
+
|
|
76
|
+
if (!testId) {
|
|
77
|
+
debug(`No test id provided for ${this.dataType} data: ${data}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
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
|
+
* This method will get data from global variable. But if it is not available, it will try to get data from file.
|
|
91
|
+
* Thus, good approach is to remove file storage folder before each test run (and after, for sure).
|
|
92
|
+
*
|
|
93
|
+
* Defining the execution environment is not guaranteed! Is used only as additional check.
|
|
94
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
getData(context) {
|
|
98
|
+
this.#refreshStorageType();
|
|
99
|
+
|
|
100
|
+
let testId = null;
|
|
101
|
+
if (typeof context === 'string') {
|
|
102
|
+
testId = this.#tryToRetrieveTestId(context) || context;
|
|
103
|
+
} else {
|
|
104
|
+
// TODO: derive testId from context
|
|
105
|
+
// testId = context...
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!testId) {
|
|
109
|
+
debug(`Cannot get test id from passed context:`, context);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let testData = '';
|
|
114
|
+
|
|
115
|
+
if (global?.testomatioDataStore) {
|
|
116
|
+
testData = this.#getDataFromGlobalVar(testId);
|
|
117
|
+
// these frameworks use global variable storage
|
|
118
|
+
if (testData && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) return testData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* condition is removed for mocha
|
|
122
|
+
mocha has created a global storage, but just in some cases, generally it is not available
|
|
123
|
+
*/
|
|
124
|
+
// if (this.isFileStorage || !testData) {
|
|
125
|
+
// testData = this.#getDataFromFile(testId);
|
|
126
|
+
// if (testData) return testData;
|
|
127
|
+
// }
|
|
128
|
+
|
|
129
|
+
const testDataFromFile = this.#getDataFromFile(testId);
|
|
130
|
+
testData += testDataFromFile || '';
|
|
131
|
+
|
|
132
|
+
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
133
|
+
return testData || '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This method is named as "try" because it does not guarantee that test id could be retrieved.
|
|
138
|
+
* Context could be anything (test, suite, string, etc) which is used to define testId.
|
|
139
|
+
* Or it could represent any other entity (which does not contain test id).
|
|
140
|
+
* @param {*} context
|
|
141
|
+
*/
|
|
142
|
+
#tryToRetrieveTestId(context) {
|
|
143
|
+
if (!context) return null;
|
|
144
|
+
this.#refreshStorageType();
|
|
145
|
+
|
|
146
|
+
if (this.runningEnvironment === 'playwright' || context?.title) {
|
|
147
|
+
// context is testInfo
|
|
148
|
+
const testId = getTestIdFromTestTitle(context.title);
|
|
149
|
+
if (testId) return testId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof context === 'string') {
|
|
153
|
+
const testId = getTestIdFromTestTitle(context);
|
|
154
|
+
if (testId) return testId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Refreshes storage type (file or global variable) depending on current environment
|
|
162
|
+
* This method should be run on each attempt to put or get data from/to storage
|
|
163
|
+
* Because storage instance is created before the test runner is started. And storage type could be changed
|
|
164
|
+
*/
|
|
165
|
+
#refreshStorageType() {
|
|
166
|
+
this.runningEnvironment = this.getRunningEnviroment();
|
|
167
|
+
|
|
168
|
+
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
169
|
+
if (['playwright', 'codeceptjs'].includes(this.runningEnvironment)) this.isFileStorage = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#getDataFromGlobalVar(testId) {
|
|
173
|
+
try {
|
|
174
|
+
if (global?.testomatioDataStore[this.dataType]) {
|
|
175
|
+
const testData = global.testomatioDataStore[this.dataType][testId];
|
|
176
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
177
|
+
return testData || '';
|
|
178
|
+
}
|
|
179
|
+
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
180
|
+
return '';
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// there could be no data, ignore
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#getDataFromFile(testId) {
|
|
187
|
+
try {
|
|
188
|
+
const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
|
|
189
|
+
if (fs.existsSync(filepath)) {
|
|
190
|
+
const testData = fs.readFileSync(filepath, 'utf-8');
|
|
191
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
192
|
+
return testData || '';
|
|
193
|
+
}
|
|
194
|
+
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
195
|
+
return '';
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// there could be no data, ignore
|
|
198
|
+
}
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#putDataToGlobalVar(data, testId) {
|
|
203
|
+
debug('Saving data to global variable for test', testId, ':\n', data, '\n');
|
|
204
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
205
|
+
if (!global.testomatioDataStore?.[this.dataType]) global.testomatioDataStore[this.dataType] = {};
|
|
206
|
+
global.testomatioDataStore?.[this.dataType][testId] // eslint-disable-line no-unused-expressions
|
|
207
|
+
? (global.testomatioDataStore[this.dataType][testId] += `\n${data}`)
|
|
208
|
+
: (global.testomatioDataStore[this.dataType][testId] = data);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#putDataToFile(data, testId) {
|
|
212
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
213
|
+
const filename = `${this.dataType}_${testId}`;
|
|
214
|
+
const filepath = join(this.dataDirPath, filename);
|
|
215
|
+
if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
|
|
216
|
+
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
217
|
+
|
|
218
|
+
// TODO: handle multiple invocations of JSON.stringify.
|
|
219
|
+
// UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
|
|
220
|
+
fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports.DataStorage = DataStorage;
|
|
225
|
+
|
|
226
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
227
|
+
// prevent blocking and improve performance (probably queue usage will be required)
|
|
228
|
+
|
|
229
|
+
// TODO: rewrite client - everything regarding storing artifacts
|
|
230
|
+
// TODO: try to define adapter inside client
|
|
231
|
+
// TODO: use .env
|
|
232
|
+
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
@@ -10,24 +10,42 @@ class JavaAdapter extends Adapter {
|
|
|
10
10
|
|
|
11
11
|
formatTest(t) {
|
|
12
12
|
const fileParts = t.suite_title.split('.')
|
|
13
|
-
const example = t.title.match(/\[(.*)\]/)?.[1];
|
|
14
|
-
if (example) t.example = { "#": example }
|
|
15
13
|
|
|
16
14
|
t.file = namespaceToFileName(t.suite_title);
|
|
17
15
|
t.title = t.title.split('(')[0];
|
|
16
|
+
|
|
17
|
+
// detect params
|
|
18
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
19
|
+
|
|
20
|
+
if (paramMatches) {
|
|
21
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
22
|
+
if (params.length === 1) params[0] = 'param';
|
|
23
|
+
let paramIndex = 0;
|
|
24
|
+
|
|
25
|
+
t.title = t.title.replace(/: \[(.*?)\]/g, () => {
|
|
26
|
+
if (params.length < 2) return `\${param}`
|
|
27
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
28
|
+
paramIndex++;
|
|
29
|
+
return `\${${paramName}}`;
|
|
30
|
+
});
|
|
31
|
+
const example = {};
|
|
32
|
+
paramMatches.forEach((match, index) => { example[params[index]] = match.replace(/[[\]]/g, '') });
|
|
33
|
+
t.example = example;
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
|
|
19
37
|
return t;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
|
-
formatStack(t) {
|
|
23
|
-
|
|
40
|
+
// formatStack(t) {
|
|
41
|
+
// const stack = super.formatStack(t);
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
// const file = t.suite_title.split('.');
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
45
|
+
// const fileLine = `at .*${file[file.length - 1]}\.java:(\\d+)` // eslint-disable-line no-useless-escape
|
|
46
|
+
// const regexp = new RegExp(fileLine,"g")
|
|
47
|
+
// return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
|
|
48
|
+
// }
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
function namespaceToFileName(fileName) {
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
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
|
+
// ! DON'T use console.log, console.warn, etc in this file, because it will lead to infinite loop
|
|
18
|
+
// use debug() instead
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logger allows to intercept logs from any logger (console.log, tracer, pino, etc)
|
|
22
|
+
* and save in the testomatio reporter.
|
|
23
|
+
* Supports different syntaxes to satisfy any user preferences.
|
|
24
|
+
*/
|
|
25
|
+
class Logger {
|
|
26
|
+
// set default logger to be used in log, warn, error, etc methods
|
|
27
|
+
#originalUserLogger = { ...console };
|
|
28
|
+
|
|
29
|
+
#dataStorage = new DataStorage('log');
|
|
30
|
+
|
|
31
|
+
logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
// intercept console by default
|
|
35
|
+
this.intercept(console);
|
|
36
|
+
|
|
37
|
+
// singleton
|
|
38
|
+
if (!Logger.instance) {
|
|
39
|
+
Logger.instance = this;
|
|
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
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Allows you to define a step inside a test. Step name is attached to the report and
|
|
59
|
+
* helps to understand the test flow.
|
|
60
|
+
* @param {*} strings
|
|
61
|
+
* @param {...any} values
|
|
62
|
+
*/
|
|
63
|
+
step(strings, ...values) {
|
|
64
|
+
// get testId for mocha
|
|
65
|
+
const context = global.testTitle ?? null;
|
|
66
|
+
|
|
67
|
+
let logs = '';
|
|
68
|
+
for (let i = 0; i < strings.length; i++) {
|
|
69
|
+
logs += strings[i];
|
|
70
|
+
if (i < values.length) {
|
|
71
|
+
logs += values[i];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
logs = chalk.blue(`> ${logs}`);
|
|
75
|
+
this.#dataStorage.putData(logs, context);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
* @param {*} context testId or test context from test runner
|
|
81
|
+
* @returns
|
|
82
|
+
*/
|
|
83
|
+
getLogs(context) {
|
|
84
|
+
const logs = this.#dataStorage.getData(context);
|
|
85
|
+
return logs || '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#stringifyLogs(...args) {
|
|
89
|
+
const logs = [];
|
|
90
|
+
// stringify everything except strings
|
|
91
|
+
for (const arg of args) {
|
|
92
|
+
// ignore empty strings
|
|
93
|
+
if (arg === '') continue;
|
|
94
|
+
if (typeof arg === 'string') {
|
|
95
|
+
logs.push(arg);
|
|
96
|
+
} else if (Array.isArray(arg)) {
|
|
97
|
+
logs.push(arg.join(' '));
|
|
98
|
+
} else {
|
|
99
|
+
try {
|
|
100
|
+
// eslint-disable-next-line no-unused-expressions
|
|
101
|
+
this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
debug('Error while stringify object', e);
|
|
104
|
+
logs.push(arg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return logs.join(' ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Tagget template literal. Allows to use different syntaxes:
|
|
113
|
+
* 1. Tagget template: log`text ${someVar}`
|
|
114
|
+
* 2. Standard: log(`text ${someVar}`)
|
|
115
|
+
* 3. Standard with multiple arguments: log('text', someVar)
|
|
116
|
+
*/
|
|
117
|
+
templateLiteralLog(strings, ...args) {
|
|
118
|
+
if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
|
|
119
|
+
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
|
+
|
|
126
|
+
let logs;
|
|
127
|
+
// this block means tagged template is used (syntax like $`text ${someVar}`)
|
|
128
|
+
if (Array.isArray(strings) && strings.length === args.length + 1) {
|
|
129
|
+
logs = strings.reduce(
|
|
130
|
+
(result, current, index) =>
|
|
131
|
+
result +
|
|
132
|
+
current +
|
|
133
|
+
// strings are splitted by args when use tagged template, thus we add arg after each string
|
|
134
|
+
// it looks like: `string1 arg1 string2 arg2 string3`
|
|
135
|
+
(args[index] !== undefined // eslint-disable-line no-nested-ternary
|
|
136
|
+
? typeof args[index] === 'string'
|
|
137
|
+
? args[index] // add arg as it is
|
|
138
|
+
: this.#stringifyLogs(args[index]) // stringify arg
|
|
139
|
+
: ''),
|
|
140
|
+
// initial accumulator value
|
|
141
|
+
'',
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
// this block means arguments syntax is used (syntax like $('text', someVar))
|
|
145
|
+
// in this case strings represents just a first argument
|
|
146
|
+
logs = this.#stringifyLogs(strings, ...args);
|
|
147
|
+
}
|
|
148
|
+
this.#originalUserLogger.log(logs);
|
|
149
|
+
this.#dataStorage.putData(logs, context);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* This function is a wrapper for each logging methods (log, warn, error etc) (not to repeat the same code)
|
|
154
|
+
* @param {*} argsArray
|
|
155
|
+
* @param {*} level
|
|
156
|
+
* @returns
|
|
157
|
+
*/
|
|
158
|
+
#logWrapper(argsArray, level) {
|
|
159
|
+
if (!argsArray.length) return;
|
|
160
|
+
|
|
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
|
+
const severity = LEVELS[level].severity;
|
|
168
|
+
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
169
|
+
|
|
170
|
+
const logs = this.#stringifyLogs(...argsArray);
|
|
171
|
+
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
172
|
+
this.#dataStorage.putData(colorizedLogs, context);
|
|
173
|
+
try {
|
|
174
|
+
// level.toLowerCase() represents method name (log, warn, error, etc)
|
|
175
|
+
this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// method could be unexisting, ignore error
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
assert(...args) {
|
|
182
|
+
this.#logWrapper(args, 'ERROR');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
debug(...args) {
|
|
186
|
+
this.#logWrapper(args, 'DEBUG');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
error(...args) {
|
|
190
|
+
this.#logWrapper(args, 'ERROR');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
info(...args) {
|
|
194
|
+
this.#logWrapper(args, 'INFO');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
log(...args) {
|
|
198
|
+
this.#logWrapper(args, 'LOG');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
trace(...args) {
|
|
202
|
+
this.#logWrapper(args, 'TRACE');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
warn(...args) {
|
|
206
|
+
this.#logWrapper(args, 'WARN');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Intercepts user logger messages.
|
|
211
|
+
* When call this method, Logger start to control the user logger
|
|
212
|
+
* @param {*} userLogger
|
|
213
|
+
*/
|
|
214
|
+
intercept(userLogger) {
|
|
215
|
+
/* prevent multiple console interceptions (cause of infinite loop)
|
|
216
|
+
actual only for "console", because its used as default output and is intercepted by default */
|
|
217
|
+
const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
|
|
218
|
+
if (isUserLoggerConsole && process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED) {
|
|
219
|
+
debug(`Try to intercept console, but it is already intercepted`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = 'true';
|
|
224
|
+
debug(isUserLoggerConsole ? 'console intercepted' : 'User logger intercepted');
|
|
225
|
+
|
|
226
|
+
// override user logger (any, e.g. console) methods to intercept log messages
|
|
227
|
+
for (const method of LOG_METHODS) {
|
|
228
|
+
/*
|
|
229
|
+
its better to create method even if it does not exist in user logger;
|
|
230
|
+
on method invocation, we will store the data anyway and catch block will prevent potential errors
|
|
231
|
+
while trying to output the message to terminal
|
|
232
|
+
*/
|
|
233
|
+
// if (!this._loggerToIntercept[method]) continue;
|
|
234
|
+
userLogger[method] = (...args) => this[method](...args);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Playwright
|
|
238
|
+
Playwright intercepts console messages by default. Thus when we intercept any logger and provide output by console,
|
|
239
|
+
messages are intercepted by Playwright.
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
Initial idea was to intercept any logger (tracer, pino, etc),
|
|
244
|
+
intercept message and provide output by the same logger.
|
|
245
|
+
But reality brings some problems: the same messages are intercepted multiple times
|
|
246
|
+
(because of multiple loggers are created at the same terminal process).
|
|
247
|
+
Also its difficult to understand (actually did not find the way to do it) if logger was already intercepted or not.
|
|
248
|
+
Thus, decided to intercept only console by default and provide output by default console.
|
|
249
|
+
It means, if user uses his own logger, its messages will be intercepted,
|
|
250
|
+
but the output will be provided by console.
|
|
251
|
+
TODO: try to implement the providing output to terminal by user logger
|
|
252
|
+
*/
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Allows to configure logger. Make sure you do it before the logger usage in your code.
|
|
257
|
+
*
|
|
258
|
+
* @param {Object} [config={}] - The configuration object.
|
|
259
|
+
* @param {string} [config.logLevel] - The desired log level. Valid values are 'DEBUG', 'INFO', 'WARN', and 'ERROR'.
|
|
260
|
+
* @param {boolean} [config.prettyObjects] - Specifies whether to enable pretty printing of objects.
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
configure(config = {}) {
|
|
264
|
+
if (!config) return;
|
|
265
|
+
if (config.prettyObjects === false || config.prettyObjects === true) this.prettyObjects = config.prettyObjects;
|
|
266
|
+
if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Logger.instance = null;
|
|
271
|
+
|
|
272
|
+
// module.exports.Logger = Logger;
|
|
273
|
+
const logger = new Logger();
|
|
274
|
+
|
|
275
|
+
module.exports = logger;
|
|
276
|
+
|
|
277
|
+
// TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
|
|
278
|
+
// upd: did not face such loggers, but still could be useful
|
|
279
|
+
|
|
280
|
+
/* Cypress
|
|
281
|
+
There is no listener like "after:test" in cypress, only "after:spec" is available.
|
|
282
|
+
Thus, cannot separate logs even when I gather them (because I don't know when the test is done, just know about suite).
|
|
283
|
+
Also there is no easy way to access the message from cy.log() function.
|
|
284
|
+
(Using testomatio logger – logger.log() is not convenient because Cypress chains its commands,
|
|
285
|
+
thus such command will interrupt the chain.)
|
|
286
|
+
|
|
287
|
+
(Could not implement intercepting of cy.log('message'));
|
|
288
|
+
I found the only ability to get any logs using .task('log', 'message') (this is custom, not default cypress command)
|
|
289
|
+
and then intercept it with:
|
|
290
|
+
on('task', {
|
|
291
|
+
log (message) {
|
|
292
|
+
console.log(message)
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
but:
|
|
298
|
+
1) it does not solve problem with getting current running testId;
|
|
299
|
+
2) leads to warning "Warning: Multiple attempts to register the following task(s):".)
|
|
300
|
+
|
|
301
|
+
My way to get test id:
|
|
302
|
+
add cypress command to save test title to file)):
|
|
303
|
+
Cypress.Commands.add('writeTestTitleToFile', () => {
|
|
304
|
+
const testTitle = cy.state('runnable').title;
|
|
305
|
+
cy.writeFile('testomatio_test_title', testTitle);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
Finally, in the test it will look like:
|
|
309
|
+
cy
|
|
310
|
+
.writeTestTitleToFile() // <<<
|
|
311
|
+
.task('log', 'This is a log message from the test') // <<<
|
|
312
|
+
|
|
313
|
+
.get('element)
|
|
314
|
+
.type('text')
|
|
315
|
+
.click()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
Parallelization in Cypress is only available if using Cypress Dashboard??
|
|
319
|
+
*/
|
package/lib/pipe/csv.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const csvWriter = require('csv-writer');
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const merge = require('lodash.merge');
|
|
7
|
-
const { isSameTest, getCurrentDateTime } = require('../
|
|
7
|
+
const { isSameTest, getCurrentDateTime } = require('../utils/utils');
|
|
8
8
|
const { CSV_HEADERS } = require('../constants');
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -40,6 +40,9 @@ class CsvPipe {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// TODO: to using SET opts as argument => prepareRun(opts)
|
|
44
|
+
async prepareRun() {}
|
|
45
|
+
|
|
43
46
|
async createRun() {
|
|
44
47
|
// empty
|
|
45
48
|
}
|