@testomatio/reporter 1.2.4-beta → 1.3.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // eslint-disable-next-line global-require, import/no-extraneous-dependencies
1
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
2
2
  const { Formatter, formatterHelpers } = require('@cucumber/cucumber');
3
3
  const chalk = require('chalk');
4
4
  const fs = require('fs');
@@ -29,7 +29,7 @@ class CucumberReporter extends Formatter {
29
29
  this.failures = [];
30
30
  this.cases = [];
31
31
 
32
- this.client = new TestomatClient();
32
+ this.client = new TestomatClient({ apiKey: options.apiKey || process.env.TESTOMATIO });
33
33
  this.status = STATUS.PASSED;
34
34
 
35
35
  global.testomatioRunningEnvironment = 'cucumber:current';
@@ -125,8 +125,8 @@ const createTestomatFormatter = apiKey => {
125
125
 
126
126
  if (!scenario.name) return;
127
127
 
128
- let message = '';
129
- let cliMessage = `- ${scenario.name}: ${chalk.bold(status.toUpperCase())}`;
128
+ const message = '';
129
+ const cliMessage = `- ${scenario.name}: ${chalk.bold(status.toUpperCase())}`;
130
130
 
131
131
  // if (event.result.status === 'undefined') {
132
132
  // cliMessage += chalk.yellow(
@@ -7,11 +7,10 @@ const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE } = require('../constan
7
7
  const TestomatioClient = require('../client');
8
8
  const { isArtifactsEnabled } = require('../fileUploader');
9
9
  const { parseTest, fileSystem } = require('../util');
10
- const { setCurrentlyRunningTestId } = require('../helpers');
11
10
 
12
11
  const reportTestPromises = [];
13
12
 
14
- class TestomatioReporter {
13
+ class PlaywrightReporter {
15
14
  constructor(config = {}) {
16
15
  this.client = new TestomatioClient({ apiKey: config?.apiKey });
17
16
 
@@ -19,24 +18,18 @@ class TestomatioReporter {
19
18
  }
20
19
 
21
20
  onBegin(_config, suite) {
21
+ // clean data storage
22
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
22
23
  if (!this.client) return;
23
24
  this.suite = suite;
24
25
  this.client.createRun();
25
-
26
- // for data storage
27
- // fileSystem.createDir(TESTOMAT_TMP_STORAGE.mainDir);
28
26
  }
29
27
 
30
- onTestBegin(test, result) {
31
- const testId = parseTest(test.title);
32
- setCurrentlyRunningTestId(testId);
33
- // global.testomatioDataStore.currentlyRunningTestId = testId;
34
- // TestomatioReporter.currentlyRunningTestId = testId;
35
- }
28
+ // onTestBegin(test) {
29
+ // const testId = parseTest(test.title);
30
+ // }
36
31
 
37
32
  onTestEnd(test, result) {
38
- // reset currently running test id
39
- // setCurrentlyRunningTestId('');
40
33
  if (!this.client) return;
41
34
 
42
35
  let testId = parseTest(test.title);
@@ -51,25 +44,23 @@ class TestomatioReporter {
51
44
  appendStep(step, steps);
52
45
  }
53
46
 
54
- const reportTestPromise = this.client
55
- .addTestRun(checkStatus(result.status), {
56
- error,
57
- test_id: testId,
58
- suite_title,
59
- title,
60
- steps: steps.join('\n'),
61
- time: duration,
62
- })
63
- .then(pipes => {
64
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
65
-
66
- this.uploads.push({
67
- testId,
68
- title,
69
- suite_title,
70
- files: result.attachments.filter(a => a.body || a.path),
71
- });
47
+ const logs = `\n\n${chalk.bold('Logs:')}\n${chalk.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
48
+
49
+ const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
50
+ error,
51
+ test_id: testId,
52
+ suite_title,
53
+ title,
54
+ steps: steps.join('\n'),
55
+ time: duration,
56
+ stack: logs,
57
+ }).then(pipes => {
58
+ testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
59
+
60
+ this.uploads.push({
61
+ testId, title, suite_title, files: result.attachments.filter((a) => a.body || a.path)
72
62
  });
63
+ });
73
64
 
74
65
  reportTestPromises.push(reportTestPromise);
75
66
  }
@@ -140,4 +131,4 @@ function tmpFile(prefix = 'tmp.') {
140
131
  return path.join(tmpdir, prefix + crypto.randomBytes(16).toString('hex'));
141
132
  }
142
133
 
143
- module.exports = TestomatioReporter;
134
+ module.exports = PlaywrightReporter;
package/lib/client.js CHANGED
@@ -38,7 +38,7 @@ class Client {
38
38
  */
39
39
  createRun() {
40
40
  // all pipes disabled, skipping
41
- if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
41
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
42
42
 
43
43
  const runParams = {
44
44
  title: this.title,
@@ -67,7 +67,7 @@ class Client {
67
67
  */
68
68
  async addTestRun(status, testData, storeArtifacts = []) {
69
69
  // all pipes disabled, skipping
70
- if (!this.pipes.filter(p => p.isEnabled).length) return;
70
+ if (!this.pipes?.filter(p => p.isEnabled).length) return [];
71
71
 
72
72
  if (!testData)
73
73
  testData = {
@@ -92,7 +92,7 @@ class Client {
92
92
 
93
93
  const uploadedFiles = [];
94
94
 
95
- let stack = testData.stack || '';
95
+ let stack = '';
96
96
 
97
97
  if (error) {
98
98
  stack = this.formatError(error) || '';
@@ -102,8 +102,10 @@ class Client {
102
102
  stack += this.formatSteps(stack, steps);
103
103
  }
104
104
 
105
+ stack += testData.stack || '';
106
+
105
107
  // in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
106
- const logger = require('./logger');
108
+ const logger = require('./logger');
107
109
  const testLogs = logger.getLogs(test_id);
108
110
  // debug(`Test logs for ${test_id}:\n`, testLogs);
109
111
  if (stack) stack += '\n\n';
@@ -151,17 +153,19 @@ class Client {
151
153
  artifacts,
152
154
  };
153
155
 
154
- this.queue = this.queue
155
- .then(() => Promise.all(this.pipes.map(async p => {
156
- try {
157
- const result = await p.addTest(data);
158
- return { pipe: p.toString(), result };
159
- } catch (err) {
160
- console.log(APP_PREFIX, p.toString(), err);
161
- }
162
- }
163
- )));
164
-
156
+ this.queue = this.queue.then(() =>
157
+ Promise.all(
158
+ this.pipes.map(async p => {
159
+ try {
160
+ const result = await p.addTest(data);
161
+ return { pipe: p.toString(), result };
162
+ } catch (err) {
163
+ console.log(APP_PREFIX, p.toString(), err);
164
+ }
165
+ }),
166
+ ),
167
+ );
168
+
165
169
  return this.queue;
166
170
  }
167
171
 
@@ -174,7 +178,7 @@ class Client {
174
178
  */
175
179
  updateRunStatus(status, isParallel = false) {
176
180
  // all pipes disabled, skipping
177
- if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
181
+ if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
178
182
 
179
183
  const runParams = { status, parallel: isParallel };
180
184
 
@@ -6,7 +6,8 @@ const { join } = require('path');
6
6
  const JestReporter = require('./adapter/jest');
7
7
  const { TESTOMAT_TMP_STORAGE } = require('./constants');
8
8
  const { fileSystem } = require('./util');
9
- // const { getCurrentlyRunningTestId } = require('./helpers');
9
+ const getTestIdFromTestTitle = require('./util').parseTest;
10
+
10
11
  class DataStorage {
11
12
  /**
12
13
  * Creates data storage instance for specific data type.
@@ -28,34 +29,6 @@ class DataStorage {
28
29
  debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
29
30
  }
30
31
 
31
- // TODO: implement
32
- // eslint-disable-next-line no-unused-vars
33
- getTestIdFromContext(context) {
34
- // return testId;
35
- }
36
-
37
- /**
38
- * Refreshes storage type (file or global variable) depending on current environment
39
- * This method should be run on each attempt to put or get data from/to storage
40
- * Because storage instance is created before the test runner is started
41
- */
42
- _refreshStorageType() {
43
- this.isFileStorage = !global.testomatioDataStore;
44
- /*
45
- FYI:
46
- If this storage instance is used within test runner, it works fine.
47
- But if, for example, we create storage instance inside the testomatio client (to get stored data),
48
- the environment could be different (not already a test runner env).
49
- Thus, checking environment is only reasonable when you put data to starage
50
- and potentially useless when get data from storage
51
- */
52
- this.runningEnvironment = this.getRunningEnviroment();
53
-
54
- // jest does not persist global variables, thus file storage is used
55
- if (this.runningEnvironment === 'jest') this.isFileStorage = true;
56
- if (this.runningEnvironment === 'playwright') this.isFileStorage = true;
57
- }
58
-
59
32
  /**
60
33
  * Try to define the running environment. Not 100% reliable and used as additional check
61
34
  * @returns jest | mocha | ...
@@ -94,18 +67,13 @@ class DataStorage {
94
67
  putData(data, context = null) {
95
68
  this._refreshStorageType();
96
69
 
97
- let testId = null;
98
- if (typeof context === 'string') {
99
- testId = context;
100
- } else {
101
- // TODO: derive testId from context
102
- // testId = context...
103
- }
70
+ let testId = this._tryToRetrieveTestId(context) || null;
104
71
 
105
- // try to get testId for Jest
106
- if (this.runningEnvironment === 'jest') testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
107
72
  if (this.runningEnvironment === 'codeceptjs') testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
108
- // if (this.runningEnvironment === 'playwright') testId = testId ?? getCurrentlyRunningTestId();
73
+ if (this.runningEnvironment === 'jest') testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
74
+ // logs in playwright are gathered by pw framwork itself
75
+ if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
76
+
109
77
  // get id from global store
110
78
  if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
111
79
  testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
@@ -164,6 +132,52 @@ class DataStorage {
164
132
  return testData;
165
133
  }
166
134
 
135
+ /**
136
+ * This method is named as "try" because it does not guarantee that test id could be retrieved.
137
+ * Context could be anything (test, suite, string, etc) which is used to define testId.
138
+ * Or it could represent any other entity (which does not contain test id).
139
+ * @param {*} context
140
+ */
141
+ _tryToRetrieveTestId(context) {
142
+ if (!context) return null;
143
+ this._refreshStorageType();
144
+
145
+ if (this.runningEnvironment === 'playwright' || context?.title) {
146
+ // context is testInfo
147
+ const testId = getTestIdFromTestTitle(context.title);
148
+ if (testId) return testId;
149
+ }
150
+
151
+ if (typeof context === 'string') {
152
+ const testId = getTestIdFromTestTitle(context);
153
+ if (testId) return testId;
154
+ }
155
+
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * Refreshes storage type (file or global variable) depending on current environment
161
+ * This method should be run on each attempt to put or get data from/to storage
162
+ * Because storage instance is created before the test runner is started. And storage type could be changed
163
+ */
164
+ _refreshStorageType() {
165
+ this.isFileStorage = !global.testomatioDataStore;
166
+ /*
167
+ FYI:
168
+ If this storage instance is used within test runner, it works fine.
169
+ But if, for example, we create storage instance inside the testomatio client (to get stored data),
170
+ the environment could differs (not already a test runner environment).
171
+ Thus, checking environment is only reasonable when you put data to starage
172
+ (to define where to put data - to global variable or to file)
173
+ and potentially useless when getting data from storage
174
+ */
175
+ this.runningEnvironment = this.getRunningEnviroment();
176
+
177
+ // some test frameworks do not persist global variables, thus file storage is used for them
178
+ if (this.runningEnvironment === 'jest') this.isFileStorage = true;
179
+ }
180
+
167
181
  _getDataFromGlobalVar(testId) {
168
182
  try {
169
183
  if (global?.testomatioDataStore[this.dataDirName]) {
@@ -224,18 +238,7 @@ module.exports.DataStorage = DataStorage;
224
238
  // TODO: use .env
225
239
  // TODO: ability to intercept multiple loggers (upd: no need)
226
240
 
227
- /* Playwright
228
- There is no global scope sharing between tests and reporter listeners.
229
- Thus need to use file storage. But when tests are run in parallel, the new problem occurs.
230
- Tried to solve it by using the TEST_WORKER_INDEX env variable, but it is available inside the listener
231
- (available only inside the test). Thus when cannot match currently running test id from the reporter listener
232
- with the currently running test.
233
- The only solution I see for now is to pass the test id to the logger from the test itself.
234
- By passing the testInfo param to the test function:
235
- async ({ page }, testInfo)
236
- and then retrieving the .title from it.
237
- */
238
241
 
239
242
  /* Cypress
240
243
  Parallelization is only available if using Cypress Dashboard??
241
- */
244
+ */
package/lib/logger.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const chalk = require('chalk');
2
2
  const debug = require('debug')('@testomatio/reporter:logger');
3
+ const _ = require('lodash');
3
4
  const { DataStorage } = require('./dataStorage');
4
5
 
5
6
  const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
@@ -34,44 +35,24 @@ class Logger {
34
35
  // commented because prefer to use "intercept" method
35
36
  // if (params?.logger) this._loggerToIntercept = params.logger;
36
37
 
37
- this.intercept(this._loggerToIntercept);
38
+ // intercept console by default
39
+ this.intercept(console);
38
40
 
39
41
  // singleton
40
42
  if (!Logger.instance) {
41
43
  Logger.instance = this;
42
44
  }
43
- }
44
45
 
45
- /**
46
- * Tagget template literal. Allows to use different syntaxes:
47
- * 1. Tagget template: $`text ${someVar}`
48
- * 2. Standard: $(`text ${someVar}`)
49
- * 3. Standard with multiple arguments: $('text', someVar)
50
- */
51
- _log(strings, ...args) {
52
- let logs;
53
- // this block means tagged template is used (syntax like $`text ${someVar}`)
54
- if (Array.isArray(strings) && strings.length === args.length + 1) {
55
- logs = strings.reduce(
56
- (result, current, index) =>
57
- result +
58
- current +
59
- // strings are splitted by args when use tagged template, thus we add arg after each string
60
- // it looks like: `string1 arg1 string2 arg2 string3`
61
- (args[index] !== undefined // eslint-disable-line no-nested-ternary
62
- ? typeof args[index] === 'string'
63
- ? args[index] // add arg as it is
64
- : this._strinfifyLogs(args[index]) // stringify arg
65
- : ' '), // add space if no arg after string
66
- // initial accumulator value
67
- '',
68
- );
69
- } else {
70
- // this block means arguments syntax is used (syntax like $('text', someVar))
71
- // in this case strings represents just a first argument
72
- logs = this._strinfifyLogs(strings, ...args);
73
- }
74
- this.dataStorage.putData(logs);
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
+ };
75
56
  }
76
57
 
77
58
  /**
@@ -102,12 +83,16 @@ class Logger {
102
83
  return logs || '';
103
84
  }
104
85
 
105
- _strinfifyLogs(...args) {
86
+ _stringifyLogs(...args) {
106
87
  const logs = [];
107
88
  // stringify everything except strings
108
89
  for (const arg of args) {
90
+ // ignore empty strings
91
+ if (arg === '') continue;
109
92
  if (typeof arg === 'string') {
110
93
  logs.push(arg);
94
+ } else if (Array.isArray(arg)) {
95
+ logs.push(arg.join(' '));
111
96
  } else {
112
97
  try {
113
98
  // eslint-disable-next-line no-unused-expressions
@@ -121,138 +106,159 @@ class Logger {
121
106
  return logs.join(' ');
122
107
  }
123
108
 
124
- assert(...args) {
125
- // sometimes user invokes logger without any arguments passed
126
- if (!args.length) return;
109
+ /**
110
+ * Tagget template literal. Allows to use different syntaxes:
111
+ * 1. Tagget template: $`text ${someVar}`
112
+ * 2. Standard: $(`text ${someVar}`)
113
+ * 3. Standard with multiple arguments: $('text', someVar)
114
+ */
115
+ _log(strings, ...args) {
116
+ // entity which is used to define testId
117
+ let context = null;
118
+ // last argument could contain testId
119
+ const testId = this._helpers.parseLastArgToGetTestId(...args);
120
+ if (testId) {
121
+ // last arg is test id, do not log it
122
+ context = args.pop();
123
+ }
127
124
 
128
- const level = 'ERROR';
129
- const severity = LEVELS[level].severity;
130
- if (severity < LEVELS[this.logLevel]?.severity) return;
125
+ // filter empty strings
126
+ strings = strings.filter(item => item !== '');
131
127
 
132
- const logs = this._strinfifyLogs(...args);
133
- this.dataStorage.putData(logs);
134
- try {
135
- this._originalUserLogger.log(`Assertion result: `, ...args);
136
- } catch (e) {
137
- // method could be unexisting, ignore error
128
+ let logs;
129
+ // this block means tagged template is used (syntax like $`text ${someVar}`)
130
+ if (Array.isArray(strings) && strings.length === args.length + 1) {
131
+ logs = strings.reduce(
132
+ (result, current, index) =>
133
+ result +
134
+ current +
135
+ // strings are splitted by args when use tagged template, thus we add arg after each string
136
+ // it looks like: `string1 arg1 string2 arg2 string3`
137
+ (args[index] !== undefined // eslint-disable-line no-nested-ternary
138
+ ? typeof args[index] === 'string'
139
+ ? args[index] // add arg as it is
140
+ : this._stringifyLogs(args[index]) // stringify arg
141
+ : ' '), // add space if no arg after string
142
+ // initial accumulator value
143
+ '',
144
+ );
145
+ } else {
146
+ // this block means arguments syntax is used (syntax like $('text', someVar))
147
+ // in this case strings represents just a first argument
148
+ logs = this._stringifyLogs(strings, ...args);
138
149
  }
150
+ this._originalUserLogger.log(logs);
151
+ this.dataStorage.putData(logs, context);
139
152
  }
140
153
 
141
- debug(...args) {
142
- if (!args.length) return;
154
+ /**
155
+ * This function is a wrapper for all logging methods (not to repeat the same code)
156
+ * @param {*} argsArray
157
+ * @param {*} level
158
+ * @returns
159
+ */
160
+ _logWrapper(argsArray, level) {
161
+ if (!argsArray.length) return;
162
+
163
+ // entity which is used to define testId
164
+ let context = null;
165
+ // last argument could contain testId
166
+ const testId = this._helpers.parseLastArgToGetTestId(...argsArray);
167
+ if (testId) {
168
+ // last arg is test id, do not log it
169
+ context = argsArray.pop();
170
+ }
143
171
 
144
- const level = 'DEBUG';
145
172
  const severity = LEVELS[level].severity;
146
173
  if (severity < LEVELS[this.logLevel]?.severity) return;
147
174
 
148
- if (this.logLevel === 'error' || this.logLevel === 'warn') return;
149
- const logs = this._strinfifyLogs(...args);
175
+ const logs = this._stringifyLogs(...argsArray);
150
176
  const colorizedLogs = chalk[LEVELS[level].color](logs);
151
- this.dataStorage.putData(colorizedLogs);
177
+ this.dataStorage.putData(colorizedLogs, context);
152
178
  try {
153
- this._originalUserLogger.debug(...args);
179
+ // level.toLowerCase() represents method name (log, warn, error, etc)
180
+ this._originalUserLogger[level.toLowerCase()](colorizedLogs);
154
181
  } catch (e) {
155
182
  // method could be unexisting, ignore error
156
183
  }
157
184
  }
158
185
 
159
- error(...args) {
186
+ assert(...args) {
187
+ // sometimes user invokes logger without any arguments passed
160
188
  if (!args.length) return;
161
189
 
162
190
  const level = 'ERROR';
163
191
  const severity = LEVELS[level].severity;
164
192
  if (severity < LEVELS[this.logLevel]?.severity) return;
165
193
 
166
- const logs = this._strinfifyLogs(...args);
167
- const colorizedLogs = chalk[LEVELS[level].color](logs);
168
- this.dataStorage.putData(colorizedLogs);
194
+ const logs = this._stringifyLogs(...args);
195
+ this.dataStorage.putData(logs);
169
196
  try {
170
- this._originalUserLogger.error(...args);
197
+ this._originalUserLogger.log(`Assertion result: `, ...args);
171
198
  } catch (e) {
172
199
  // method could be unexisting, ignore error
173
200
  }
174
201
  }
175
202
 
176
- info(...args) {
177
- if (!args.length) return;
203
+ debug(...args) {
204
+ this._logWrapper(args, 'DEBUG');
205
+ }
178
206
 
179
- const level = 'INFO';
180
- const severity = LEVELS[level].severity;
181
- if (severity < LEVELS[this.logLevel]?.severity) return;
207
+ error(...args) {
208
+ this._logWrapper(args, 'ERROR');
209
+ }
182
210
 
183
- const logs = this._strinfifyLogs(...args);
184
- this.dataStorage.putData(logs);
185
- try {
186
- this._originalUserLogger.info(...args);
187
- } catch (e) {
188
- // method could be unexisting, ignore error
189
- }
211
+ info(...args) {
212
+ this._logWrapper(args, 'INFO');
190
213
  }
191
214
 
192
215
  log(...args) {
193
- if (!args.length) return;
194
-
195
- const level = 'INFO';
196
- const severity = LEVELS[level].severity;
197
- if (severity < LEVELS[this.logLevel]?.severity) return;
198
-
199
- const logs = this._strinfifyLogs(...args);
200
- this.dataStorage.putData(logs);
201
- try {
202
- this._originalUserLogger.log(...args);
203
- } catch (e) {
204
- // method could be unexisting, ignore error
205
- }
216
+ this._logWrapper(args, 'LOG');
206
217
  }
207
218
 
208
219
  trace(...args) {
209
- if (!args.length) return;
210
-
211
- const level = 'TRACE';
212
- const severity = LEVELS[level].severity;
213
- if (severity < LEVELS[this.logLevel]?.severity) return;
214
-
215
- const logs = this._strinfifyLogs(...args);
216
- const colorizedLogs = chalk[LEVELS[level].color](logs);
217
- this.dataStorage.putData(colorizedLogs);
218
- try {
219
- this._originalUserLogger.trace(...args);
220
- } catch (e) {
221
- // method could be unexisting, ignore error
222
- }
220
+ this._logWrapper(args, 'TRACE');
223
221
  }
224
222
 
225
223
  warn(...args) {
226
- if (!args.length) return;
227
-
228
- const level = 'WARN';
229
- const severity = LEVELS[level].severity;
230
- if (severity < LEVELS[this.logLevel]?.severity) return;
231
-
232
- const logs = this._strinfifyLogs(...args);
233
- const colorizedLogs = chalk[LEVELS[level].color](logs);
234
- this.dataStorage.putData(colorizedLogs);
235
- try {
236
- this._originalUserLogger.warn(...args);
237
- } catch (e) {
238
- // method could be unexisting, ignore error
239
- }
224
+ this._logWrapper(args, 'WARN');
240
225
  }
241
226
 
242
227
  /**
243
228
  * Intercepts user logger messages.
244
229
  * When call this method, Logger start to control the user logger,
245
- * but almost nothing is changed for user ragarding the console output (like log level set by user)
230
+ * but almost nothing is changed for user regarding the console output (like log level set by user)
246
231
  * (until multiple loggers are intercepted,
247
232
  * in this case only the last intercepted logger will be used as user console output).
248
233
  * @param {*} userLogger
249
234
  */
250
235
  intercept(userLogger) {
251
236
  if (!userLogger) return;
252
- debug(`User logger intercepted`);
253
237
 
254
- // save original user logger to use it for logging (last intercepted will be used for console output)
255
- this._originalUserLogger = { ...userLogger };
238
+ /* prevent multiple console interceptions (cause infinite loop)
239
+ actual only for "console", because its used as default output and is intercepted by default */
240
+ const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
241
+ if (isUserLoggerConsole && process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED) {
242
+ debug(`Try to intercept console, but it is already intercepted`);
243
+ return;
244
+ }
245
+ // prevent other loggers multiple interceptions
246
+ if (_.isEqual(userLogger, this._originalUserLogger)) {
247
+ debug(`Try to intercept user logger, but it is already intercepted`);
248
+ return;
249
+ }
250
+
251
+ process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = 'true';
252
+ debug(isUserLoggerConsole ? 'console intercepted' : 'User logger intercepted');
253
+
254
+ /* logs could be intercepted for playwright only if console provides output, thus adding this exception.
255
+ it means default _originalUserLogger (console by default) will be used to provide output
256
+ (for other frameworks user logger will be passed for output) */
257
+ if (this.dataStorage.runningEnvironment !== 'playwright') {
258
+ // save original user logger to use it for logging (last intercepted will be used for console output)
259
+ //! TODO: temporary don't oeverride user logger to prevent recursion; console will be used for output
260
+ // this._originalUserLogger = { ...userLogger };
261
+ }
256
262
  this._loggerToIntercept = userLogger;
257
263
 
258
264
  /*
@@ -287,7 +293,9 @@ class Logger {
287
293
  Logger.instance = null;
288
294
 
289
295
  // module.exports.Logger = Logger;
290
- module.exports = new Logger();
296
+ const logger = new Logger();
297
+
298
+ module.exports = logger;
291
299
 
292
300
  // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
293
301
  // upd: did not face such loggers, but still could be useful
package/lib/util.js CHANGED
@@ -12,7 +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
  const captures = testTitle.match(/@T([\w\d]+)/);
17
+
16
18
  if (captures) {
17
19
  return captures[1];
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.2.4-beta",
3
+ "version": "1.3.1-beta",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -25,6 +25,7 @@
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",
28
29
  "lodash.memoize": "^4.1.2",
29
30
  "lodash.merge": "^4.6.2",
30
31
  "uuid": "^9.0.0"
@@ -51,10 +52,12 @@
51
52
  "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js"
52
53
  },
53
54
  "devDependencies": {
55
+ "@cucumber/cucumber": "^9.3.0",
54
56
  "@redocly/cli": "^1.0.0-beta.125",
55
57
  "@wdio/reporter": "^7.16.13",
56
58
  "chai": "^4.3.6",
57
59
  "codeceptjs": "^3.2.3",
60
+ "cucumber": "^6.0.7",
58
61
  "eslint": "^8.7.0",
59
62
  "eslint-config-airbnb-base": "^15.0.0",
60
63
  "eslint-config-prettier": "^8.3.0",
package/lib/helpers.js DELETED
@@ -1,34 +0,0 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const { TESTOMAT_TMP_STORAGE } = require('./constants');
4
-
5
- // NEXT HELPERS ARE CREATED FOR PLAYWRIGHT. IGNORE FOR NOW. PROBABLY WILL BE REMOVED SHORTLY
6
-
7
- // TEST_WORKER_INDEX is not available inside the test listener :/
8
- /**
9
- * Sets id of currently running test to storage (global variable or file)
10
- * Used for Playwright
11
- * @param {*} testId
12
- */
13
- function setCurrentlyRunningTestIdForPlaywright(testId) {
14
- const workerId = +process.env.TEST_WORKER_INDEX || 0;
15
- const filePath = path.join(TESTOMAT_TMP_STORAGE.mainDir, 'current_test_id_' + workerId);
16
- fs.writeFileSync(filePath, testId);
17
- }
18
-
19
- /**
20
- * Gets id of currently running test from storage (global variable or file)
21
- * Used for Playwright
22
- * @returns string test id of currently running test
23
- */
24
- function getCurrentlyRunningTestIdForPlaywright() {
25
- const workerId = +process.env.TEST_WORKER_INDEX || 0;
26
- const filePath = path.join(TESTOMAT_TMP_STORAGE.mainDir, 'current_test_id_' + workerId);
27
- if (fs.existsSync(filePath)) {
28
- return fs.readFileSync(filePath, 'utf8') || null;
29
- }
30
- return null;
31
- };
32
-
33
- module.exports.setCurrentlyRunningTestId = setCurrentlyRunningTestIdForPlaywright;
34
- module.exports.getCurrentlyRunningTestId = getCurrentlyRunningTestIdForPlaywright;