@testomatio/reporter 1.0.11 → 1.0.12
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/lib/artifactStorage.js +2 -2
- package/lib/dataStorage.js +34 -57
- package/lib/logger.js +67 -109
- package/lib/reporter.js +2 -1
- package/lib/util.js +56 -44
- package/package.json +4 -4
package/lib/artifactStorage.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
const { DataStorage } = require('./dataStorage');
|
|
3
3
|
|
|
4
4
|
class ArtifactStorage {
|
|
5
|
-
constructor(
|
|
6
|
-
this.dataStorage = new DataStorage('artifact'
|
|
5
|
+
constructor() {
|
|
6
|
+
this.dataStorage = new DataStorage('artifact');
|
|
7
7
|
|
|
8
8
|
// singleton
|
|
9
9
|
if (!ArtifactStorage.instance) {
|
package/lib/dataStorage.js
CHANGED
|
@@ -13,21 +13,15 @@ class DataStorage {
|
|
|
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
|
-
* dataType: 'log' | 'artifact' | ...
|
|
17
16
|
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
18
17
|
*/
|
|
19
18
|
constructor(dataType) {
|
|
20
|
-
// if (!dataType) throw new Error('Data type is required when creating data storage');
|
|
21
19
|
this.dataType = dataType || 'data';
|
|
22
|
-
this.dataDirName = this.dataType;
|
|
23
20
|
this.isFileStorage = true;
|
|
24
|
-
this
|
|
21
|
+
this.#refreshStorageType();
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataDirName);
|
|
23
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataType);
|
|
28
24
|
fileSystem.createDir(this.dataDirPath);
|
|
29
|
-
|
|
30
|
-
debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
/**
|
|
@@ -38,18 +32,14 @@ class DataStorage {
|
|
|
38
32
|
// jest
|
|
39
33
|
if (process.env.JEST_WORKER_ID) return 'jest';
|
|
40
34
|
|
|
41
|
-
// codeceptjs
|
|
42
|
-
// @ts-expect-error codeceptjs is defined only in codeceptjs environment
|
|
43
35
|
if (global.codeceptjs) return 'codeceptjs';
|
|
44
36
|
|
|
45
|
-
// others
|
|
46
37
|
// 'cucumber:current', 'cucumber:legacy'
|
|
47
38
|
if (global.testomatioRunningEnvironment) return global.testomatioRunningEnvironment;
|
|
48
39
|
|
|
49
|
-
if (process.env.PLAYWRIGHT_TEST_BASE_URL) return 'playwright';
|
|
40
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL || process.env.TEST_WORKER_INDEX) return 'playwright';
|
|
50
41
|
|
|
51
42
|
// mocha - can't detect
|
|
52
|
-
|
|
53
43
|
return null;
|
|
54
44
|
}
|
|
55
45
|
|
|
@@ -60,18 +50,19 @@ class DataStorage {
|
|
|
60
50
|
* @returns
|
|
61
51
|
*/
|
|
62
52
|
putData(data, context = null) {
|
|
63
|
-
this
|
|
53
|
+
this.#refreshStorageType();
|
|
64
54
|
|
|
65
|
-
let testId = this
|
|
55
|
+
let testId = this.#tryToRetrieveTestId(context) || null;
|
|
66
56
|
|
|
67
57
|
if (this.runningEnvironment === 'codeceptjs') {
|
|
68
58
|
this.isFileStorage = false;
|
|
69
59
|
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
70
60
|
}
|
|
61
|
+
|
|
71
62
|
if (this.runningEnvironment === 'jest') {
|
|
72
63
|
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
73
|
-
this.isFileStorage = true;
|
|
74
64
|
}
|
|
65
|
+
|
|
75
66
|
// logs in playwright are gathered by pw framework itself
|
|
76
67
|
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
|
|
77
68
|
|
|
@@ -80,18 +71,17 @@ class DataStorage {
|
|
|
80
71
|
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
81
72
|
|
|
82
73
|
if (!testId && global?.currentlyRunningTestTitle)
|
|
83
|
-
testId = this
|
|
74
|
+
testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
84
75
|
|
|
85
|
-
// if testId is not provided, data is be saved to `{dataType}_other` file;
|
|
86
76
|
if (!testId) {
|
|
87
77
|
debug(`No test id provided for ${this.dataType} data: ${data}`);
|
|
88
78
|
return;
|
|
89
79
|
}
|
|
90
80
|
|
|
91
81
|
if (this.isFileStorage) {
|
|
92
|
-
this
|
|
82
|
+
this.#putDataToFile(data, testId);
|
|
93
83
|
} else {
|
|
94
|
-
this
|
|
84
|
+
this.#putDataToGlobalVar(data, testId);
|
|
95
85
|
}
|
|
96
86
|
}
|
|
97
87
|
|
|
@@ -105,25 +95,25 @@ class DataStorage {
|
|
|
105
95
|
* @returns
|
|
106
96
|
*/
|
|
107
97
|
getData(context) {
|
|
108
|
-
this
|
|
98
|
+
this.#refreshStorageType();
|
|
109
99
|
|
|
110
100
|
let testId = null;
|
|
111
101
|
if (typeof context === 'string') {
|
|
112
|
-
testId = context;
|
|
102
|
+
testId = this.#tryToRetrieveTestId(context) || context;
|
|
113
103
|
} else {
|
|
114
104
|
// TODO: derive testId from context
|
|
115
105
|
// testId = context...
|
|
116
106
|
}
|
|
117
107
|
|
|
118
108
|
if (!testId) {
|
|
119
|
-
debug(`Cannot get test id from passed context
|
|
109
|
+
debug(`Cannot get test id from passed context:`, context);
|
|
120
110
|
return null;
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
let testData = '';
|
|
124
114
|
|
|
125
115
|
if (global?.testomatioDataStore) {
|
|
126
|
-
testData = this
|
|
116
|
+
testData = this.#getDataFromGlobalVar(testId);
|
|
127
117
|
// these frameworks use global variable storage
|
|
128
118
|
if (testData && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) return testData;
|
|
129
119
|
}
|
|
@@ -132,17 +122,15 @@ class DataStorage {
|
|
|
132
122
|
mocha has created a global storage, but just in some cases, generally it is not available
|
|
133
123
|
*/
|
|
134
124
|
// if (this.isFileStorage || !testData) {
|
|
135
|
-
// testData = this
|
|
125
|
+
// testData = this.#getDataFromFile(testId);
|
|
136
126
|
// if (testData) return testData;
|
|
137
127
|
// }
|
|
138
128
|
|
|
139
|
-
const testDataFromFile = this
|
|
129
|
+
const testDataFromFile = this.#getDataFromFile(testId);
|
|
140
130
|
testData += testDataFromFile || '';
|
|
141
131
|
|
|
142
|
-
if (testData) return testData;
|
|
143
|
-
|
|
144
132
|
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
145
|
-
return testData;
|
|
133
|
+
return testData || '';
|
|
146
134
|
}
|
|
147
135
|
|
|
148
136
|
/**
|
|
@@ -151,9 +139,9 @@ class DataStorage {
|
|
|
151
139
|
* Or it could represent any other entity (which does not contain test id).
|
|
152
140
|
* @param {*} context
|
|
153
141
|
*/
|
|
154
|
-
|
|
142
|
+
#tryToRetrieveTestId(context) {
|
|
155
143
|
if (!context) return null;
|
|
156
|
-
this
|
|
144
|
+
this.#refreshStorageType();
|
|
157
145
|
|
|
158
146
|
if (this.runningEnvironment === 'playwright' || context?.title) {
|
|
159
147
|
// context is testInfo
|
|
@@ -174,28 +162,19 @@ class DataStorage {
|
|
|
174
162
|
* This method should be run on each attempt to put or get data from/to storage
|
|
175
163
|
* Because storage instance is created before the test runner is started. And storage type could be changed
|
|
176
164
|
*/
|
|
177
|
-
|
|
178
|
-
/*
|
|
179
|
-
FYI:
|
|
180
|
-
If this storage instance is used within test runner, it works fine.
|
|
181
|
-
But if, for example, we create storage instance inside the testomatio client (to get stored data),
|
|
182
|
-
the environment could differs (not already a test runner environment).
|
|
183
|
-
Thus, checking environment is only reasonable when you put data to starage
|
|
184
|
-
(to define where to put data - to global variable or to file)
|
|
185
|
-
and potentially useless when getting data from storage
|
|
186
|
-
*/
|
|
165
|
+
#refreshStorageType() {
|
|
187
166
|
this.runningEnvironment = this.getRunningEnviroment();
|
|
188
167
|
|
|
189
168
|
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
190
169
|
if (['playwright', 'codeceptjs'].includes(this.runningEnvironment)) this.isFileStorage = false;
|
|
191
170
|
}
|
|
192
171
|
|
|
193
|
-
|
|
172
|
+
#getDataFromGlobalVar(testId) {
|
|
194
173
|
try {
|
|
195
|
-
if (global?.testomatioDataStore[this.
|
|
196
|
-
const testData = global.testomatioDataStore[this.
|
|
174
|
+
if (global?.testomatioDataStore[this.dataType]) {
|
|
175
|
+
const testData = global.testomatioDataStore[this.dataType][testId];
|
|
197
176
|
debug(`Data for test id ${testId}:\n${testData}`);
|
|
198
|
-
return testData;
|
|
177
|
+
return testData || '';
|
|
199
178
|
}
|
|
200
179
|
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
201
180
|
return '';
|
|
@@ -204,13 +183,13 @@ class DataStorage {
|
|
|
204
183
|
}
|
|
205
184
|
}
|
|
206
185
|
|
|
207
|
-
|
|
186
|
+
#getDataFromFile(testId) {
|
|
208
187
|
try {
|
|
209
188
|
const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
|
|
210
189
|
if (fs.existsSync(filepath)) {
|
|
211
190
|
const testData = fs.readFileSync(filepath, 'utf-8');
|
|
212
191
|
debug(`Data for test id ${testId}:\n${testData}`);
|
|
213
|
-
return testData;
|
|
192
|
+
return testData || '';
|
|
214
193
|
}
|
|
215
194
|
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
216
195
|
return '';
|
|
@@ -220,18 +199,20 @@ class DataStorage {
|
|
|
220
199
|
return '';
|
|
221
200
|
}
|
|
222
201
|
|
|
223
|
-
|
|
202
|
+
#putDataToGlobalVar(data, testId) {
|
|
224
203
|
debug('Saving data to global variable for test', testId, ':\n', data, '\n');
|
|
225
|
-
if (!global.testomatioDataStore
|
|
226
|
-
global.testomatioDataStore[this.
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
229
209
|
}
|
|
230
210
|
|
|
231
|
-
|
|
211
|
+
#putDataToFile(data, testId) {
|
|
232
212
|
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
233
213
|
const filename = `${this.dataType}_${testId}`;
|
|
234
214
|
const filepath = join(this.dataDirPath, filename);
|
|
215
|
+
if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
|
|
235
216
|
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
236
217
|
|
|
237
218
|
// TODO: handle multiple invocations of JSON.stringify.
|
|
@@ -249,7 +230,3 @@ module.exports.DataStorage = DataStorage;
|
|
|
249
230
|
// TODO: try to define adapter inside client
|
|
250
231
|
// TODO: use .env
|
|
251
232
|
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
252
|
-
|
|
253
|
-
/* Cypress
|
|
254
|
-
Parallelization is only available if using Cypress Dashboard??
|
|
255
|
-
*/
|
package/lib/logger.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const debug = require('debug')('@testomatio/reporter:logger');
|
|
3
|
-
const _ = require('lodash');
|
|
4
3
|
const { DataStorage } = require('./dataStorage');
|
|
5
4
|
|
|
6
5
|
const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
|
|
@@ -15,26 +14,23 @@ const LEVELS = {
|
|
|
15
14
|
ERROR: { severity: 15, color: 'red' },
|
|
16
15
|
};
|
|
17
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
|
+
|
|
18
20
|
/**
|
|
19
|
-
* Logger allows to
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* 3. Varied syntax.
|
|
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.
|
|
23
24
|
*/
|
|
24
25
|
class Logger {
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
constructor() {
|
|
29
|
-
// set default logger to be used in log, warn, error, etc methods
|
|
30
|
-
this._originalUserLogger = { ...console };
|
|
26
|
+
// set default logger to be used in log, warn, error, etc methods
|
|
27
|
+
#originalUserLogger = { ...console };
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
29
|
+
#dataStorage = new DataStorage('log');
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
// if (params?.logger) this._loggerToIntercept = params.logger;
|
|
31
|
+
logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
37
32
|
|
|
33
|
+
constructor() {
|
|
38
34
|
// intercept console by default
|
|
39
35
|
this.intercept(console);
|
|
40
36
|
|
|
@@ -43,22 +39,11 @@ class Logger {
|
|
|
43
39
|
Logger.instance = this;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
this._helpers = {
|
|
47
|
-
parseLastArgToGetTestId: (...args) => {
|
|
48
|
-
try {
|
|
49
|
-
return this.dataStorage._tryToRetrieveTestId(args.at(-1));
|
|
50
|
-
} catch (e) {
|
|
51
|
-
// node 14 support
|
|
52
|
-
return this.dataStorage._tryToRetrieveTestId(args[args.length - 1]);
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
42
|
// add beforeEach hook for mocha. it does not override existing hook, just add new one
|
|
58
43
|
try {
|
|
59
44
|
// @ts-ignore
|
|
60
45
|
if (!beforeEach) return;
|
|
61
|
-
// @ts-ignore
|
|
46
|
+
// @ts-ignore-next-line
|
|
62
47
|
beforeEach(function () {
|
|
63
48
|
if (this.currentTest?.__mocha_id__) {
|
|
64
49
|
global.testTitle = this.currentTest.fullTitle();
|
|
@@ -76,6 +61,9 @@ class Logger {
|
|
|
76
61
|
* @param {...any} values
|
|
77
62
|
*/
|
|
78
63
|
step(strings, ...values) {
|
|
64
|
+
// get testId for mocha
|
|
65
|
+
const context = global.testTitle ?? null;
|
|
66
|
+
|
|
79
67
|
let logs = '';
|
|
80
68
|
for (let i = 0; i < strings.length; i++) {
|
|
81
69
|
logs += strings[i];
|
|
@@ -84,7 +72,7 @@ class Logger {
|
|
|
84
72
|
}
|
|
85
73
|
}
|
|
86
74
|
logs = chalk.blue(`> ${logs}`);
|
|
87
|
-
this
|
|
75
|
+
this.#dataStorage.putData(logs, context);
|
|
88
76
|
}
|
|
89
77
|
|
|
90
78
|
/**
|
|
@@ -93,11 +81,11 @@ class Logger {
|
|
|
93
81
|
* @returns
|
|
94
82
|
*/
|
|
95
83
|
getLogs(context) {
|
|
96
|
-
const logs = this
|
|
84
|
+
const logs = this.#dataStorage.getData(context);
|
|
97
85
|
return logs || '';
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
|
|
88
|
+
#stringifyLogs(...args) {
|
|
101
89
|
const logs = [];
|
|
102
90
|
// stringify everything except strings
|
|
103
91
|
for (const arg of args) {
|
|
@@ -122,27 +110,19 @@ class Logger {
|
|
|
122
110
|
|
|
123
111
|
/**
|
|
124
112
|
* Tagget template literal. Allows to use different syntaxes:
|
|
125
|
-
* 1. Tagget template:
|
|
126
|
-
* 2. Standard:
|
|
127
|
-
* 3. Standard with multiple arguments:
|
|
113
|
+
* 1. Tagget template: log`text ${someVar}`
|
|
114
|
+
* 2. Standard: log(`text ${someVar}`)
|
|
115
|
+
* 3. Standard with multiple arguments: log('text', someVar)
|
|
128
116
|
*/
|
|
129
|
-
|
|
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 !== '');
|
|
130
120
|
// entity which is used to define testId
|
|
131
121
|
let context = null;
|
|
132
122
|
|
|
133
123
|
// get testId for mocha
|
|
134
124
|
context = global.testTitle ?? null;
|
|
135
125
|
|
|
136
|
-
// last argument could contain testId
|
|
137
|
-
const testId = this._helpers.parseLastArgToGetTestId(...args);
|
|
138
|
-
if (testId) {
|
|
139
|
-
// last arg is test id, do not log it
|
|
140
|
-
context = args.pop();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// filter empty strings
|
|
144
|
-
strings = strings.filter(item => item !== '');
|
|
145
|
-
|
|
146
126
|
let logs;
|
|
147
127
|
// this block means tagged template is used (syntax like $`text ${someVar}`)
|
|
148
128
|
if (Array.isArray(strings) && strings.length === args.length + 1) {
|
|
@@ -155,18 +135,18 @@ class Logger {
|
|
|
155
135
|
(args[index] !== undefined // eslint-disable-line no-nested-ternary
|
|
156
136
|
? typeof args[index] === 'string'
|
|
157
137
|
? args[index] // add arg as it is
|
|
158
|
-
: this
|
|
159
|
-
: '
|
|
138
|
+
: this.#stringifyLogs(args[index]) // stringify arg
|
|
139
|
+
: ''),
|
|
160
140
|
// initial accumulator value
|
|
161
141
|
'',
|
|
162
142
|
);
|
|
163
143
|
} else {
|
|
164
144
|
// this block means arguments syntax is used (syntax like $('text', someVar))
|
|
165
145
|
// in this case strings represents just a first argument
|
|
166
|
-
logs = this
|
|
146
|
+
logs = this.#stringifyLogs(strings, ...args);
|
|
167
147
|
}
|
|
168
|
-
this.
|
|
169
|
-
this
|
|
148
|
+
this.#originalUserLogger.log(logs);
|
|
149
|
+
this.#dataStorage.putData(logs, context);
|
|
170
150
|
}
|
|
171
151
|
|
|
172
152
|
/**
|
|
@@ -175,7 +155,7 @@ class Logger {
|
|
|
175
155
|
* @param {*} level
|
|
176
156
|
* @returns
|
|
177
157
|
*/
|
|
178
|
-
|
|
158
|
+
#logWrapper(argsArray, level) {
|
|
179
159
|
if (!argsArray.length) return;
|
|
180
160
|
|
|
181
161
|
// entity which is used to define testId
|
|
@@ -184,117 +164,92 @@ class Logger {
|
|
|
184
164
|
// get context for mocha
|
|
185
165
|
context = global.testTitle ?? null;
|
|
186
166
|
|
|
187
|
-
// last argument could contain testId
|
|
188
|
-
const testId = this._helpers.parseLastArgToGetTestId(...argsArray);
|
|
189
|
-
if (testId) {
|
|
190
|
-
// last arg is test id, do not log it
|
|
191
|
-
context = argsArray.pop();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
167
|
const severity = LEVELS[level].severity;
|
|
195
168
|
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
196
169
|
|
|
197
|
-
const logs = this
|
|
170
|
+
const logs = this.#stringifyLogs(...argsArray);
|
|
198
171
|
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
199
|
-
this
|
|
172
|
+
this.#dataStorage.putData(colorizedLogs, context);
|
|
200
173
|
try {
|
|
201
174
|
// level.toLowerCase() represents method name (log, warn, error, etc)
|
|
202
|
-
this
|
|
175
|
+
this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
|
|
203
176
|
} catch (e) {
|
|
204
177
|
// method could be unexisting, ignore error
|
|
205
178
|
}
|
|
206
179
|
}
|
|
207
180
|
|
|
208
181
|
assert(...args) {
|
|
209
|
-
|
|
210
|
-
if (!args.length) return;
|
|
211
|
-
|
|
212
|
-
const level = 'ERROR';
|
|
213
|
-
const severity = LEVELS[level].severity;
|
|
214
|
-
if (severity < LEVELS[this.logLevel]?.severity) return;
|
|
215
|
-
|
|
216
|
-
const logs = this._stringifyLogs(...args);
|
|
217
|
-
this.dataStorage.putData(logs);
|
|
218
|
-
try {
|
|
219
|
-
this._originalUserLogger.log(`Assertion result: `, ...args);
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// method could be unexisting, ignore error
|
|
222
|
-
}
|
|
182
|
+
this.#logWrapper(args, 'ERROR');
|
|
223
183
|
}
|
|
224
184
|
|
|
225
185
|
debug(...args) {
|
|
226
|
-
this
|
|
186
|
+
this.#logWrapper(args, 'DEBUG');
|
|
227
187
|
}
|
|
228
188
|
|
|
229
189
|
error(...args) {
|
|
230
|
-
this
|
|
190
|
+
this.#logWrapper(args, 'ERROR');
|
|
231
191
|
}
|
|
232
192
|
|
|
233
193
|
info(...args) {
|
|
234
|
-
this
|
|
194
|
+
this.#logWrapper(args, 'INFO');
|
|
235
195
|
}
|
|
236
196
|
|
|
237
197
|
log(...args) {
|
|
238
|
-
this
|
|
198
|
+
this.#logWrapper(args, 'LOG');
|
|
239
199
|
}
|
|
240
200
|
|
|
241
201
|
trace(...args) {
|
|
242
|
-
this
|
|
202
|
+
this.#logWrapper(args, 'TRACE');
|
|
243
203
|
}
|
|
244
204
|
|
|
245
205
|
warn(...args) {
|
|
246
|
-
this
|
|
206
|
+
this.#logWrapper(args, 'WARN');
|
|
247
207
|
}
|
|
248
208
|
|
|
249
209
|
/**
|
|
250
210
|
* Intercepts user logger messages.
|
|
251
|
-
* When call this method, Logger start to control the user logger
|
|
252
|
-
* but almost nothing is changed for user regarding the console output (like log level set by user)
|
|
253
|
-
* (until multiple loggers are intercepted,
|
|
254
|
-
* in this case only the last intercepted logger will be used as user console output).
|
|
211
|
+
* When call this method, Logger start to control the user logger
|
|
255
212
|
* @param {*} userLogger
|
|
256
213
|
*/
|
|
257
214
|
intercept(userLogger) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
/* prevent multiple console interceptions (cause infinite loop)
|
|
215
|
+
/* prevent multiple console interceptions (cause of infinite loop)
|
|
261
216
|
actual only for "console", because its used as default output and is intercepted by default */
|
|
262
217
|
const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
|
|
263
218
|
if (isUserLoggerConsole && process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED) {
|
|
264
219
|
debug(`Try to intercept console, but it is already intercepted`);
|
|
265
220
|
return;
|
|
266
221
|
}
|
|
267
|
-
// prevent other loggers multiple interceptions
|
|
268
|
-
if (_.isEqual(userLogger, this._originalUserLogger)) {
|
|
269
|
-
debug(`Try to intercept user logger, but it is already intercepted`);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
222
|
|
|
273
223
|
process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = 'true';
|
|
274
224
|
debug(isUserLoggerConsole ? 'console intercepted' : 'User logger intercepted');
|
|
275
225
|
|
|
276
|
-
|
|
277
|
-
it means default _originalUserLogger (console by default) will be used to provide output
|
|
278
|
-
(for other frameworks user logger will be passed for output) */
|
|
279
|
-
if (this.dataStorage.runningEnvironment !== 'playwright') {
|
|
280
|
-
// save original user logger to use it for logging (last intercepted will be used for console output)
|
|
281
|
-
//! TODO: temporary don't oeverride user logger to prevent recursion; console will be used for output
|
|
282
|
-
// this._originalUserLogger = { ...userLogger };
|
|
283
|
-
}
|
|
284
|
-
this._loggerToIntercept = userLogger;
|
|
285
|
-
|
|
286
|
-
/*
|
|
287
|
-
override user logger (any, e.g. console) methods to intercept log messages
|
|
288
|
-
this._loggerToIntercept = this; could be used, but decided to override only output methods
|
|
289
|
-
*/
|
|
226
|
+
// override user logger (any, e.g. console) methods to intercept log messages
|
|
290
227
|
for (const method of LOG_METHODS) {
|
|
291
228
|
/*
|
|
292
|
-
|
|
229
|
+
its better to create method even if it does not exist in user logger;
|
|
293
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
|
|
294
232
|
*/
|
|
295
233
|
// if (!this._loggerToIntercept[method]) continue;
|
|
296
|
-
|
|
234
|
+
userLogger[method] = (...args) => this[method](...args);
|
|
297
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
|
+
*/
|
|
298
253
|
}
|
|
299
254
|
|
|
300
255
|
/**
|
|
@@ -307,7 +262,7 @@ class Logger {
|
|
|
307
262
|
*/
|
|
308
263
|
configure(config = {}) {
|
|
309
264
|
if (!config) return;
|
|
310
|
-
if (config.prettyObjects) this.prettyObjects = config.prettyObjects;
|
|
265
|
+
if (config.prettyObjects === false || config.prettyObjects === true) this.prettyObjects = config.prettyObjects;
|
|
311
266
|
if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
|
|
312
267
|
}
|
|
313
268
|
}
|
|
@@ -358,4 +313,7 @@ Finally, in the test it will look like:
|
|
|
358
313
|
.get('element)
|
|
359
314
|
.type('text')
|
|
360
315
|
.click()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
Parallelization in Cypress is only available if using Cypress Dashboard??
|
|
361
319
|
*/
|
package/lib/reporter.js
CHANGED
|
@@ -3,11 +3,12 @@ const TestomatClient = require('./client');
|
|
|
3
3
|
const TRConstants = require('./constants');
|
|
4
4
|
const TRArtifacts = require('./_ArtifactStorageOld');
|
|
5
5
|
|
|
6
|
-
const log = logger.
|
|
6
|
+
const log = logger.templateLiteralLog.bind(logger);
|
|
7
7
|
const step = logger.step.bind(logger);
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
logger,
|
|
11
|
+
testomatioLogger: logger,
|
|
11
12
|
log,
|
|
12
13
|
step,
|
|
13
14
|
TestomatClient,
|
package/lib/util.js
CHANGED
|
@@ -12,9 +12,9 @@ const debug = require('debug')('@testomatio/reporter:util');
|
|
|
12
12
|
*/
|
|
13
13
|
const parseTest = testTitle => {
|
|
14
14
|
if (!testTitle) return null;
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
const captures = testTitle.match(/@T([\w\d]+)/);
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
if (captures) {
|
|
19
19
|
return captures[1];
|
|
20
20
|
}
|
|
@@ -87,11 +87,13 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
87
87
|
|
|
88
88
|
if (!source) return '';
|
|
89
89
|
|
|
90
|
-
return source
|
|
90
|
+
return source
|
|
91
|
+
.split('\n')
|
|
91
92
|
.map((l, i) => {
|
|
92
93
|
if (i === prepend) return `${line} > ${chalk.bold(l)}`;
|
|
93
|
-
return `${line - prepend + i} | ${l}
|
|
94
|
-
})
|
|
94
|
+
return `${line - prepend + i} | ${l}`;
|
|
95
|
+
})
|
|
96
|
+
.join('\n');
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
@@ -115,7 +117,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
115
117
|
|
|
116
118
|
if (lineIndex) {
|
|
117
119
|
const result = [];
|
|
118
|
-
for (let i = lineIndex; i <
|
|
120
|
+
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
119
121
|
if (lines[i] === undefined) continue;
|
|
120
122
|
|
|
121
123
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
@@ -189,54 +191,63 @@ const fileSystem = {
|
|
|
189
191
|
} else {
|
|
190
192
|
debug(`Trying to delete ${dirPath} but it doesn't exist`);
|
|
191
193
|
}
|
|
192
|
-
}
|
|
194
|
+
},
|
|
193
195
|
};
|
|
194
196
|
|
|
195
|
-
const humanize =
|
|
197
|
+
const humanize = text => {
|
|
196
198
|
text = decamelize(text);
|
|
197
|
-
return text
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
return text
|
|
200
|
+
.replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
|
|
201
|
+
.trim()
|
|
202
|
+
.replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
|
|
203
|
+
.trim()
|
|
204
|
+
.replace(/\sA\s/g, ' a ') // replace a|the
|
|
205
|
+
.replace(/\sThe\s/g, ' the ') // replace a|the
|
|
206
|
+
.replace(/^Test\s/, '')
|
|
207
|
+
.replace(/^Should\s/, '');
|
|
208
|
+
};
|
|
205
209
|
|
|
206
210
|
/**
|
|
207
211
|
* From https://github.com/sindresorhus/decamelize/blob/main/index.js
|
|
208
|
-
* @param {*} text
|
|
209
|
-
* @returns
|
|
212
|
+
* @param {*} text
|
|
213
|
+
* @returns
|
|
210
214
|
*/
|
|
211
|
-
const decamelize =
|
|
215
|
+
const decamelize = text => {
|
|
212
216
|
const separator = '_';
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
/(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
|
|
236
|
-
(_, $1, $2) => $1 + separator + $2.toLowerCase(),
|
|
237
|
-
);
|
|
217
|
+
const replacement = `$1${separator}$2`;
|
|
218
|
+
|
|
219
|
+
// Split lowercase sequences followed by uppercase character.
|
|
220
|
+
// `dataForUSACounties` → `data_For_USACounties`
|
|
221
|
+
// `myURLstring → `my_URLstring`
|
|
222
|
+
let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
|
|
223
|
+
|
|
224
|
+
// Lowercase all single uppercase characters. As we
|
|
225
|
+
// want to preserve uppercase sequences, we cannot
|
|
226
|
+
// simply lowercase the separated string at the end.
|
|
227
|
+
// `data_For_USACounties` → `data_for_USACounties`
|
|
228
|
+
decamelized = decamelized.replace(
|
|
229
|
+
/((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
|
|
230
|
+
$0 => $0.toLowerCase(),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Remaining uppercase sequences will be separated from lowercase sequences.
|
|
234
|
+
// `data_For_USACounties` → `data_for_USA_counties`
|
|
235
|
+
return decamelized.replace(
|
|
236
|
+
/(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
|
|
237
|
+
(_, $1, $2) => $1 + separator + $2.toLowerCase(),
|
|
238
|
+
);
|
|
238
239
|
};
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Used to remove color codes
|
|
243
|
+
* @param {*} input
|
|
244
|
+
* @returns
|
|
245
|
+
*/
|
|
246
|
+
function removeColorCodes(input) {
|
|
247
|
+
// eslint-disable-next-line no-control-regex
|
|
248
|
+
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
249
|
+
}
|
|
250
|
+
|
|
240
251
|
module.exports = {
|
|
241
252
|
isSameTest,
|
|
242
253
|
fetchSourceCode,
|
|
@@ -250,4 +261,5 @@ module.exports = {
|
|
|
250
261
|
parseTest,
|
|
251
262
|
parseSuite,
|
|
252
263
|
humanize,
|
|
264
|
+
removeColorCodes,
|
|
253
265
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"main": "./lib/reporter.js",
|
|
6
6
|
"typings": "typings/index.d.ts",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"humanize-duration": "^3.27.3",
|
|
26
26
|
"is-valid-path": "^0.1.1",
|
|
27
27
|
"json-cycle": "^1.3.0",
|
|
28
|
-
"lodash": "^4.17.21",
|
|
29
28
|
"lodash.memoize": "^4.1.2",
|
|
30
29
|
"lodash.merge": "^4.6.2",
|
|
31
30
|
"uuid": "^9.0.0"
|
|
@@ -42,14 +41,14 @@
|
|
|
42
41
|
"lint:fix": "eslint lib --fix",
|
|
43
42
|
"test": "mocha tests/**",
|
|
44
43
|
"init": "cd ./tests/adapter/examples/cucumber && npm i",
|
|
45
|
-
"test:unit": "mocha tests",
|
|
46
44
|
"test:adapter": "mocha './tests/adapter/index.test.js'",
|
|
47
45
|
"test:pipes": "mocha './tests/pipes/*_test.js'",
|
|
48
46
|
"test:adapter:jest:example": "jest './tests/adapter/examples/jest/index.test.js' --config='./tests/adapter/examples/jest/jest.config.js'",
|
|
49
47
|
"test:adapter:mocha:example": "mocha './tests/adapter/examples/mocha/index.test.js' --config='./tests/adapter/examples/mocha/mocha.config.js'",
|
|
50
48
|
"test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",
|
|
51
49
|
"test:adapter:codecept:example": "codeceptjs run --config='./tests/adapter/examples/codecept/codecept.conf.js'",
|
|
52
|
-
"test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js"
|
|
50
|
+
"test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js",
|
|
51
|
+
"test:storage": "npx mocha ./tests-storage/**"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
55
54
|
"@cucumber/cucumber": "^9.3.0",
|
|
@@ -66,6 +65,7 @@
|
|
|
66
65
|
"jest": "^27.4.7",
|
|
67
66
|
"mocha": "^9.2.0",
|
|
68
67
|
"mock-http-server": "^1.4.5",
|
|
68
|
+
"pino": "^8.15.0",
|
|
69
69
|
"prettier": "2.5.1",
|
|
70
70
|
"puppeteer": "^13.1.2"
|
|
71
71
|
},
|