@testomatio/reporter 1.0.12-beta.4 → 1.0.13

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 CHANGED
@@ -1,4 +1,28 @@
1
1
  <!-- pending release updates -->
2
+ # 1.0.13
3
+
4
+ * JUnit improvements
5
+ * Match test from source code by adding Test ID as a comment:
6
+
7
+ ```java
8
+ // @T8acca9eb
9
+ ```
10
+ * Match test from output by adding Test ID as output:
11
+
12
+ ```java
13
+ System.out.println("tid://@T8acca9eb");
14
+ ```
15
+ * Support for suite before and after output
16
+ * Improved support for artifacts
17
+
18
+ # 1.0.12
19
+
20
+ & Logger refactoring by @olexandr13 in #208
21
+ * fix undefined logs by @olexandr13 in #210
22
+
23
+ # 1.0.11
24
+
25
+ * fix steps duplication for codecept report by @olexandr13 in #209
2
26
 
3
27
  # 1.0.10
4
28
 
@@ -2,8 +2,8 @@
2
2
  const { DataStorage } = require('./dataStorage');
3
3
 
4
4
  class ArtifactStorage {
5
- constructor(params = {}) {
6
- this.dataStorage = new DataStorage('artifact', params);
5
+ constructor() {
6
+ this.dataStorage = new DataStorage('artifact');
7
7
 
8
8
  // singleton
9
9
  if (!ArtifactStorage.instance) {
package/lib/client.js CHANGED
@@ -92,7 +92,7 @@ class Client {
92
92
  message = error?.message;
93
93
  }
94
94
  if (steps) {
95
- stack += this.formatSteps(stack, steps);
95
+ stack = this.formatSteps(stack, steps);
96
96
  }
97
97
 
98
98
  stack += testData.stack || '';
@@ -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._refreshStorageType();
21
+ this.#refreshStorageType();
25
22
 
26
- // dir is created in any case (not to recheck its existence every time)
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._refreshStorageType();
53
+ this.#refreshStorageType();
64
54
 
65
- let testId = this._tryToRetrieveTestId(context) || null;
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._tryToRetrieveTestId(global.currentlyRunningTestTitle);
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._putDataToFile(data, testId);
82
+ this.#putDataToFile(data, testId);
93
83
  } else {
94
- this._putDataToGlobalVar(data, testId);
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._refreshStorageType();
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:\n}`, 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._getDataFromGlobalVar(testId);
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._getDataFromFile(testId);
125
+ // testData = this.#getDataFromFile(testId);
136
126
  // if (testData) return testData;
137
127
  // }
138
128
 
139
- const testDataFromFile = this._getDataFromFile(testId);
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
- _tryToRetrieveTestId(context) {
142
+ #tryToRetrieveTestId(context) {
155
143
  if (!context) return null;
156
- this._refreshStorageType();
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
- _refreshStorageType() {
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
- _getDataFromGlobalVar(testId) {
172
+ #getDataFromGlobalVar(testId) {
194
173
  try {
195
- if (global?.testomatioDataStore[this.dataDirName]) {
196
- const testData = global.testomatioDataStore[this.dataDirName][testId];
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
- _getDataFromFile(testId) {
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
- _putDataToGlobalVar(data, testId) {
202
+ #putDataToGlobalVar(data, testId) {
224
203
  debug('Saving data to global variable for test', testId, ':\n', data, '\n');
225
- if (!global.testomatioDataStore[this.dataDirName]) global.testomatioDataStore[this.dataDirName] = {};
226
- global.testomatioDataStore[this.dataDirName][testId] // eslint-disable-line no-unused-expressions
227
- ? (global.testomatioDataStore[this.dataDirName][testId] += `\n${data}`)
228
- : (global.testomatioDataStore[this.dataDirName][testId] = data);
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
- _putDataToFile(data, testId) {
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
- * 1. Intercept logs from user logger (console.log, etc) and store them.
21
- * 2. Output logs to console (actually, logger functionality).
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
- // _originalUserLogger used to output logs to console by the user logger
26
- // _loggerToIntercept intercepted and reassigned immediately when added
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
- this.dataStorage = new DataStorage('log');
33
- this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
29
+ #dataStorage = new DataStorage('log');
34
30
 
35
- // commented because prefer to use "intercept" method
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.dataStorage.putData(logs);
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.dataStorage.getData(context);
84
+ const logs = this.#dataStorage.getData(context);
97
85
  return logs || '';
98
86
  }
99
87
 
100
- _stringifyLogs(...args) {
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: $`text ${someVar}`
126
- * 2. Standard: $(`text ${someVar}`)
127
- * 3. Standard with multiple arguments: $('text', someVar)
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
- _log(strings, ...args) {
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._stringifyLogs(args[index]) // stringify arg
159
- : ' '), // add space if no arg after string
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._stringifyLogs(strings, ...args);
146
+ logs = this.#stringifyLogs(strings, ...args);
167
147
  }
168
- this._originalUserLogger.log(logs);
169
- this.dataStorage.putData(logs, context);
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
- _logWrapper(argsArray, level) {
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._stringifyLogs(...argsArray);
170
+ const logs = this.#stringifyLogs(...argsArray);
198
171
  const colorizedLogs = chalk[LEVELS[level].color](logs);
199
- this.dataStorage.putData(colorizedLogs, context);
172
+ this.#dataStorage.putData(colorizedLogs, context);
200
173
  try {
201
174
  // level.toLowerCase() represents method name (log, warn, error, etc)
202
- this._originalUserLogger[level.toLowerCase()](colorizedLogs);
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
- // sometimes user invokes logger without any arguments passed
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._logWrapper(args, 'DEBUG');
186
+ this.#logWrapper(args, 'DEBUG');
227
187
  }
228
188
 
229
189
  error(...args) {
230
- this._logWrapper(args, 'ERROR');
190
+ this.#logWrapper(args, 'ERROR');
231
191
  }
232
192
 
233
193
  info(...args) {
234
- this._logWrapper(args, 'INFO');
194
+ this.#logWrapper(args, 'INFO');
235
195
  }
236
196
 
237
197
  log(...args) {
238
- this._logWrapper(args, 'LOG');
198
+ this.#logWrapper(args, 'LOG');
239
199
  }
240
200
 
241
201
  trace(...args) {
242
- this._logWrapper(args, 'TRACE');
202
+ this.#logWrapper(args, 'TRACE');
243
203
  }
244
204
 
245
205
  warn(...args) {
246
- this._logWrapper(args, 'WARN');
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
- if (!userLogger) return;
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
- /* logs could be intercepted for playwright only if console provides output, thus adding this exception.
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
- decided to comment next code line because its better to create method even if it does not exist in user logger;
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
- this._loggerToIntercept[method] = (...args) => this[method](...args);
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._log.bind(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
  }
@@ -97,11 +97,13 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
97
97
 
98
98
  if (!source) return '';
99
99
 
100
- return source.split('\n')
100
+ return source
101
+ .split('\n')
101
102
  .map((l, i) => {
102
103
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
103
- return `${line - prepend + i} | ${l}`
104
- }).join('\n');
104
+ return `${line - prepend + i} | ${l}`;
105
+ })
106
+ .join('\n');
105
107
  };
106
108
 
107
109
  const TEST_ID_REGEX = /@T([\w\d]{8})/;
@@ -120,6 +122,13 @@ const fetchIdFromCode = (code, opts = {}) => {
120
122
  return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
121
123
  }
122
124
 
125
+
126
+ const fetchIdFromOutput = (output) => {
127
+ const lines = output.split('\n').map(l => l.trim()).filter(l => l.startsWith('tid://'));
128
+
129
+ return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
130
+ }
131
+
123
132
  const fetchSourceCode = (contents, opts = {}) => {
124
133
  if (!opts.title && !opts.line) return '';
125
134
 
@@ -141,7 +150,7 @@ const fetchSourceCode = (contents, opts = {}) => {
141
150
 
142
151
  if (lineIndex) {
143
152
  const result = [];
144
- for (let i = lineIndex; i < (lineIndex + limit); i++) {
153
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
145
154
  if (lines[i] === undefined) continue;
146
155
 
147
156
  if (i > lineIndex + 2 && !opts.prepend) {
@@ -215,59 +224,69 @@ const fileSystem = {
215
224
  } else {
216
225
  debug(`Trying to delete ${dirPath} but it doesn't exist`);
217
226
  }
218
- }
227
+ },
219
228
  };
220
229
 
221
- const humanize = (text) => {
230
+ const humanize = text => {
222
231
  text = decamelize(text);
223
- return text.replace(/_./g, match => ` ${ match.charAt(1).toUpperCase()}`)
224
- .trim()
225
- .replace(/^(.)|\s(.)/g, ($1) => $1.toUpperCase()).trim()
226
- .replace(/\sA\s/g, ' a ') // replace a|the
227
- .replace(/\sThe\s/g, ' the ') // replace a|the
228
- .replace(/^Test\s/, '')
229
- .replace(/^Should\s/, '')
230
- }
232
+ return text
233
+ .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
234
+ .trim()
235
+ .replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
236
+ .trim()
237
+ .replace(/\sA\s/g, ' a ') // replace a|the
238
+ .replace(/\sThe\s/g, ' the ') // replace a|the
239
+ .replace(/^Test\s/, '')
240
+ .replace(/^Should\s/, '');
241
+ };
231
242
 
232
243
  /**
233
244
  * From https://github.com/sindresorhus/decamelize/blob/main/index.js
234
- * @param {*} text
235
- * @returns
245
+ * @param {*} text
246
+ * @returns
236
247
  */
237
- const decamelize = (text) => {
248
+ const decamelize = text => {
238
249
  const separator = '_';
239
- const replacement = `$1${separator}$2`;
240
-
241
- // Split lowercase sequences followed by uppercase character.
242
- // `dataForUSACounties` → `data_For_USACounties`
243
- // `myURLstring → `my_URLstring`
244
- let decamelized = text.replace(
245
- /([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu,
246
- replacement,
247
- );
248
-
249
- // Lowercase all single uppercase characters. As we
250
- // want to preserve uppercase sequences, we cannot
251
- // simply lowercase the separated string at the end.
252
- // `data_For_USACounties` → `data_for_USACounties`
253
- decamelized = decamelized.replace(
254
- /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
255
- $0 => $0.toLowerCase(),
256
- );
257
-
258
- // Remaining uppercase sequences will be separated from lowercase sequences.
259
- // `data_For_USACounties` `data_for_USA_counties`
260
- return decamelized.replace(
261
- /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
262
- (_, $1, $2) => $1 + separator + $2.toLowerCase(),
263
- );
250
+ const replacement = `$1${separator}$2`;
251
+
252
+ // Split lowercase sequences followed by uppercase character.
253
+ // `dataForUSACounties` → `data_For_USACounties`
254
+ // `myURLstring → `my_URLstring`
255
+ let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
256
+
257
+ // Lowercase all single uppercase characters. As we
258
+ // want to preserve uppercase sequences, we cannot
259
+ // simply lowercase the separated string at the end.
260
+ // `data_For_USACounties` `data_for_USACounties`
261
+ decamelized = decamelized.replace(
262
+ /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
263
+ $0 => $0.toLowerCase(),
264
+ );
265
+
266
+ // Remaining uppercase sequences will be separated from lowercase sequences.
267
+ // `data_For_USACounties` → `data_for_USA_counties`
268
+ return decamelized.replace(
269
+ /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
270
+ (_, $1, $2) => $1 + separator + $2.toLowerCase(),
271
+ );
264
272
  };
265
273
 
274
+ /**
275
+ * Used to remove color codes
276
+ * @param {*} input
277
+ * @returns
278
+ */
279
+ function removeColorCodes(input) {
280
+ // eslint-disable-next-line no-control-regex
281
+ return input.replace(/\x1b\[[0-9;]*m/g, '');
282
+ }
283
+
266
284
  module.exports = {
267
285
  isSameTest,
268
286
  fetchSourceCode,
269
287
  fetchSourceCodeFromStackTrace,
270
288
  fetchIdFromCode,
289
+ fetchIdFromOutput,
271
290
  fetchFilesFromStackTrace,
272
291
  fileSystem,
273
292
  getCurrentDateTime,
@@ -277,4 +296,5 @@ module.exports = {
277
296
  parseTest,
278
297
  parseSuite,
279
298
  humanize,
299
+ removeColorCodes,
280
300
  };
package/lib/xmlReader.js CHANGED
@@ -4,7 +4,12 @@ const chalk = require('chalk');
4
4
  const fs = require("fs");
5
5
  const { XMLParser } = require("fast-xml-parser");
6
6
  const { APP_PREFIX, STATUS } = require('./constants');
7
- const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace, fetchIdFromCode } = require('./util');
7
+ const { fetchFilesFromStackTrace,
8
+ fetchIdFromOutput,
9
+ fetchSourceCode,
10
+ fetchSourceCodeFromStackTrace,
11
+ fetchIdFromCode
12
+ } = require('./util');
8
13
  const upload = require('./fileUploader');
9
14
  const pipesFactory = require('./pipe');
10
15
  const adapterFactory = require('./junit-adapter');
@@ -25,7 +30,7 @@ const options = {
25
30
  const reduceOptions = {};
26
31
 
27
32
  class XmlReader {
28
-
33
+
29
34
  constructor(opts = {}) {
30
35
  this.requestParams = {
31
36
  apiKey: opts.apiKey || TESTOMATIO,
@@ -49,7 +54,7 @@ class XmlReader {
49
54
  this.filesToUpload = {}
50
55
 
51
56
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
52
- console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
57
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
53
58
  }
54
59
 
55
60
  connectAdapter() {
@@ -61,21 +66,21 @@ class XmlReader {
61
66
  return this.adapter;
62
67
  }
63
68
 
64
- parse(fileName) {
69
+ parse(fileName) {
65
70
  const xmlData = fs.readFileSync(path.resolve(fileName));
66
71
  const jsonResult = this.parser.parse(xmlData);
67
72
  let jsonSuite;
68
-
73
+
69
74
  if (jsonResult.testsuites) {
70
75
  jsonSuite = jsonResult.testsuites;
71
76
  } else if (jsonResult.testsuite) {
72
77
  jsonSuite = jsonResult;
73
78
  } else if (jsonResult.TestRun) {
74
- return this.processTRX(jsonResult);
79
+ return this.processTRX(jsonResult);
75
80
  } else if (jsonResult['test-run']) {
76
81
  return this.processNUnit(jsonResult['test-run']);
77
82
  } else if (jsonResult.assemblies) {
78
- return this.processXUnit(jsonResult.assemblies);
83
+ return this.processXUnit(jsonResult.assemblies);
79
84
  } else {
80
85
  console.log(jsonResult)
81
86
  throw new Error("Format can't be parsed")
@@ -89,9 +94,9 @@ class XmlReader {
89
94
 
90
95
  reduceOptions.preferClassname = this.stats.language === 'python';
91
96
  const resultTests = processTestSuite(testsuite);
92
-
97
+
93
98
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
94
- const status = ( failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
99
+ const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
95
100
 
96
101
  this.tests = this.tests.concat(resultTests);
97
102
 
@@ -124,7 +129,7 @@ class XmlReader {
124
129
  skipped_count: parseInt(inconclusive + skipped, 10),
125
130
  tests: resultTests,
126
131
  };
127
- }
132
+ }
128
133
 
129
134
  processTRX(jsonSuite) {
130
135
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
@@ -133,10 +138,10 @@ class XmlReader {
133
138
  const tests = defs.map(td => {
134
139
  const title = td.name.replace(/\(.*?\)/, '').trim();
135
140
  let example = td.name.match(/\((.*?)\)/);
136
- if (example) example = { ...example[1].split(',')};
141
+ if (example) example = { ...example[1].split(',') };
137
142
  const suite = td.TestMethod.className.split(', ')[0].split('.');
138
143
  const suite_title = suite.pop();
139
- return {
144
+ return {
140
145
  title,
141
146
  example,
142
147
  file: suite.join('/'),
@@ -149,18 +154,18 @@ class XmlReader {
149
154
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
150
155
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
151
156
 
152
- const results = result.map(td => ({
157
+ const results = result.map(td => ({
153
158
  id: td.executionId,
154
159
  // seconds are used in junit reports, but ms are used by testomatio
155
160
  run_time: parseFloat(td.duration) * 1000,
156
- status: td.outcome,
161
+ status: td.outcome,
157
162
  stack: td.Output.StdOut,
158
163
  files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
159
164
  }));
160
165
 
161
-
166
+
162
167
  results.forEach(r => {
163
- const test = tests.find(t => t.id === r.id) || { };
168
+ const test = tests.find(t => t.id === r.id) || {};
164
169
  r.suite_title = test.suite_title;
165
170
  r.title = test.title?.trim();
166
171
  if (test.code) r.code = test.code;
@@ -175,7 +180,7 @@ class XmlReader {
175
180
  });
176
181
 
177
182
  debug(results);
178
-
183
+
179
184
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
180
185
 
181
186
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
@@ -193,7 +198,7 @@ class XmlReader {
193
198
  skipped_count: parseInt(counters.notExecuted, 10),
194
199
  failed_count,
195
200
  tests: results,
196
- };
201
+ };
197
202
  }
198
203
 
199
204
  processXUnit(assemblies) {
@@ -227,7 +232,7 @@ class XmlReader {
227
232
  let status = STATUS.PASSED;
228
233
  if (result === 'Pass') status = STATUS.PASSED;
229
234
  if (result === 'Fail') status = STATUS.FAILED;
230
- if (result === 'Skip') status = STATUS.SKIPPED;
235
+ if (result === 'Skip') status = STATUS.SKIPPED;
231
236
 
232
237
  const pathParts = type.split('.');
233
238
  const suite_title = pathParts[pathParts.length - 1];
@@ -300,7 +305,7 @@ class XmlReader {
300
305
  if (file.endsWith('.py')) this.stats.language = 'python';
301
306
  if (file.endsWith('.java')) this.stats.language = 'java';
302
307
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
303
- if (file.endsWith('.js')) this.stats.language = 'js';
308
+ if (file.endsWith('.js')) this.stats.language = 'js';
304
309
  if (file.endsWith('.ts')) this.stats.language = 'ts';
305
310
  }
306
311
 
@@ -311,7 +316,7 @@ class XmlReader {
311
316
  const contents = fs.readFileSync(file).toString();
312
317
  t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
313
318
  if (t.code) debug('Fetched code for test %s', t.title);
314
- t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language } )
319
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
315
320
  if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
316
321
  } catch (err) {
317
322
  debug(err)
@@ -356,7 +361,7 @@ class XmlReader {
356
361
  let files = [];
357
362
  if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
358
363
  files = [...files, ...fetchFilesFromStackTrace(test.stack)];
359
-
364
+
360
365
  if (!files.length) continue;
361
366
 
362
367
  const runId = this.runId || this.store.runId || Date.now().toString();
@@ -371,8 +376,8 @@ class XmlReader {
371
376
  title: this.requestParams.title,
372
377
  env: this.requestParams.env,
373
378
  group_title: this.requestParams.group_title,
374
- };
375
-
379
+ };
380
+
376
381
  debug("Run", runParams);
377
382
 
378
383
  return Promise.all(this.pipes.map(p => p.createRun(runParams)));
@@ -389,9 +394,9 @@ class XmlReader {
389
394
  debug(
390
395
  'Uploading data',
391
396
  {
392
- ...this.stats,
393
- tests: this.tests,
394
- })
397
+ ...this.stats,
398
+ tests: this.tests,
399
+ })
395
400
 
396
401
  const dataString = {
397
402
  ...this.stats,
@@ -430,8 +435,9 @@ function reduceTestCases(prev, item) {
430
435
  if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
431
436
  if (!message) message = stack.trim().split('\n')[0];
432
437
 
433
- // prepend system output
438
+ // eslint-disable-next-line
434
439
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
440
+ const testId = fetchIdFromOutput(stack);
435
441
 
436
442
  let status = STATUS.PASSED.toString();
437
443
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
@@ -441,6 +447,7 @@ function reduceTestCases(prev, item) {
441
447
  create: true,
442
448
  file,
443
449
  stack,
450
+ test_id: testId,
444
451
  message,
445
452
  line: testCaseItem.lineno,
446
453
  // seconds are used in junit reports, but ms are used by testomatio
@@ -463,7 +470,7 @@ function processTestSuite(testsuite) {
463
470
  suites = [testsuite];
464
471
  }
465
472
 
466
- const res = suites.reduce(reduceTestCases, []);
473
+ const res = suites.reduce(reduceTestCases, []);
467
474
 
468
475
  return res;
469
476
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.0.12-beta.4",
3
+ "version": "1.0.13",
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
  },