@testomatio/reporter 1.3.6-beta → 1.4.1-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
1
  const chalk = require('chalk');
2
2
  const debug = require('debug')('@testomatio/reporter:logger');
3
- const _ = require('lodash');
4
- const { DataStorage } = require('./dataStorage');
3
+ const DataStorage = require('./data-storage');
5
4
 
6
5
  const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
7
6
  const LEVELS = {
@@ -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
 
@@ -42,17 +38,6 @@ class Logger {
42
38
  if (!Logger.instance) {
43
39
  Logger.instance = this;
44
40
  }
45
-
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
41
  }
57
42
 
58
43
  /**
@@ -70,7 +55,7 @@ class Logger {
70
55
  }
71
56
  }
72
57
  logs = chalk.blue(`> ${logs}`);
73
- this.dataStorage.putData(logs);
58
+ this.#dataStorage.putData(logs);
74
59
  }
75
60
 
76
61
  /**
@@ -79,11 +64,11 @@ class Logger {
79
64
  * @returns
80
65
  */
81
66
  getLogs(context) {
82
- const logs = this.dataStorage.getData(context);
67
+ const logs = this.#dataStorage.getData(context);
83
68
  return logs || '';
84
69
  }
85
70
 
86
- _stringifyLogs(...args) {
71
+ #stringifyLogs(...args) {
87
72
  const logs = [];
88
73
  // stringify everything except strings
89
74
  for (const arg of args) {
@@ -108,22 +93,13 @@ class Logger {
108
93
 
109
94
  /**
110
95
  * 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)
96
+ * 1. Tagget template: log`text ${someVar}`
97
+ * 2. Standard: log(`text ${someVar}`)
98
+ * 3. Standard with multiple arguments: log('text', someVar)
114
99
  */
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
- }
124
-
125
- // filter empty strings
126
- strings = strings.filter(item => item !== '');
100
+ templateLiteralLog(strings, ...args) {
101
+ if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
102
+ if (Array.isArray(args)) args = args.filter(item => item !== '');
127
103
 
128
104
  let logs;
129
105
  // this block means tagged template is used (syntax like $`text ${someVar}`)
@@ -137,142 +113,115 @@ class Logger {
137
113
  (args[index] !== undefined // eslint-disable-line no-nested-ternary
138
114
  ? typeof args[index] === 'string'
139
115
  ? args[index] // add arg as it is
140
- : this._stringifyLogs(args[index]) // stringify arg
141
- : ' '), // add space if no arg after string
116
+ : this.#stringifyLogs(args[index]) // stringify arg
117
+ : ''),
142
118
  // initial accumulator value
143
119
  '',
144
120
  );
145
121
  } else {
146
122
  // this block means arguments syntax is used (syntax like $('text', someVar))
147
123
  // in this case strings represents just a first argument
148
- logs = this._stringifyLogs(strings, ...args);
124
+ logs = this.#stringifyLogs(strings, ...args);
149
125
  }
150
- this._originalUserLogger.log(logs);
151
- this.dataStorage.putData(logs, context);
126
+ this.#originalUserLogger.log(logs);
127
+ this.#dataStorage.putData(logs);
152
128
  }
153
129
 
154
130
  /**
155
- * This function is a wrapper for all logging methods (not to repeat the same code)
131
+ * This function is a wrapper for each logging methods (log, warn, error etc) (not to repeat the same code)
156
132
  * @param {*} argsArray
157
133
  * @param {*} level
158
134
  * @returns
159
135
  */
160
- _logWrapper(argsArray, level) {
136
+ #logWrapper(argsArray, level) {
161
137
  if (!argsArray.length) return;
162
138
 
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
- }
171
-
172
139
  const severity = LEVELS[level].severity;
173
140
  if (severity < LEVELS[this.logLevel]?.severity) return;
174
141
 
175
- const logs = this._stringifyLogs(...argsArray);
142
+ const logs = this.#stringifyLogs(...argsArray);
176
143
  const colorizedLogs = chalk[LEVELS[level].color](logs);
177
- this.dataStorage.putData(colorizedLogs, context);
144
+ this.#dataStorage.putData(colorizedLogs);
178
145
  try {
179
146
  // level.toLowerCase() represents method name (log, warn, error, etc)
180
- this._originalUserLogger[level.toLowerCase()](colorizedLogs);
147
+ this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
181
148
  } catch (e) {
182
149
  // method could be unexisting, ignore error
183
150
  }
184
151
  }
185
152
 
186
153
  assert(...args) {
187
- // sometimes user invokes logger without any arguments passed
188
- if (!args.length) return;
189
-
190
- const level = 'ERROR';
191
- const severity = LEVELS[level].severity;
192
- if (severity < LEVELS[this.logLevel]?.severity) return;
193
-
194
- const logs = this._stringifyLogs(...args);
195
- this.dataStorage.putData(logs);
196
- try {
197
- this._originalUserLogger.log(`Assertion result: `, ...args);
198
- } catch (e) {
199
- // method could be unexisting, ignore error
200
- }
154
+ this.#logWrapper(args, 'ERROR');
201
155
  }
202
156
 
203
157
  debug(...args) {
204
- this._logWrapper(args, 'DEBUG');
158
+ this.#logWrapper(args, 'DEBUG');
205
159
  }
206
160
 
207
161
  error(...args) {
208
- this._logWrapper(args, 'ERROR');
162
+ this.#logWrapper(args, 'ERROR');
209
163
  }
210
164
 
211
165
  info(...args) {
212
- this._logWrapper(args, 'INFO');
166
+ this.#logWrapper(args, 'INFO');
213
167
  }
214
168
 
215
169
  log(...args) {
216
- this._logWrapper(args, 'LOG');
170
+ this.#logWrapper(args, 'LOG');
217
171
  }
218
172
 
219
173
  trace(...args) {
220
- this._logWrapper(args, 'TRACE');
174
+ this.#logWrapper(args, 'TRACE');
221
175
  }
222
176
 
223
177
  warn(...args) {
224
- this._logWrapper(args, 'WARN');
178
+ this.#logWrapper(args, 'WARN');
225
179
  }
226
180
 
227
181
  /**
228
182
  * Intercepts user logger messages.
229
- * When call this method, Logger start to control the user logger,
230
- * but almost nothing is changed for user regarding the console output (like log level set by user)
231
- * (until multiple loggers are intercepted,
232
- * in this case only the last intercepted logger will be used as user console output).
183
+ * When call this method, Logger start to control the user logger
233
184
  * @param {*} userLogger
234
185
  */
235
186
  intercept(userLogger) {
236
- if (!userLogger) return;
237
-
238
- /* prevent multiple console interceptions (cause infinite loop)
187
+ /* prevent multiple console interceptions (cause of infinite loop)
239
188
  actual only for "console", because its used as default output and is intercepted by default */
240
189
  const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
241
190
  if (isUserLoggerConsole && process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED) {
242
191
  debug(`Try to intercept console, but it is already intercepted`);
243
192
  return;
244
193
  }
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
194
 
251
195
  process.env.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = 'true';
252
196
  debug(isUserLoggerConsole ? 'console intercepted' : 'User logger intercepted');
253
197
 
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
- }
262
- this._loggerToIntercept = userLogger;
263
-
264
- /*
265
- override user logger (any, e.g. console) methods to intercept log messages
266
- this._loggerToIntercept = this; could be used, but decided to override only output methods
267
- */
198
+ // override user logger (any, e.g. console) methods to intercept log messages
268
199
  for (const method of LOG_METHODS) {
269
200
  /*
270
- decided to comment next code line because its better to create method even if it does not exist in user logger;
201
+ its better to create method even if it does not exist in user logger;
271
202
  on method invocation, we will store the data anyway and catch block will prevent potential errors
203
+ while trying to output the message to terminal
272
204
  */
273
205
  // if (!this._loggerToIntercept[method]) continue;
274
- this._loggerToIntercept[method] = (...args) => this[method](...args);
206
+ userLogger[method] = (...args) => this[method](...args);
275
207
  }
208
+
209
+ /* Playwright
210
+ Playwright intercepts console messages by default. Thus when we intercept any logger and provide output by console,
211
+ messages are intercepted by Playwright.
212
+ */
213
+
214
+ /*
215
+ Initial idea was to intercept any logger (tracer, pino, etc),
216
+ intercept message and provide output by the same logger.
217
+ But reality brings some problems: the same messages are intercepted multiple times
218
+ (because of multiple loggers are created at the same terminal process).
219
+ Also its difficult to understand (actually did not find the way to do it) if logger was already intercepted or not.
220
+ Thus, decided to intercept only console by default and provide output by default console.
221
+ It means, if user uses his own logger, its messages will be intercepted,
222
+ but the output will be provided by console.
223
+ TODO: try to implement the providing output to terminal by user logger
224
+ */
276
225
  }
277
226
 
278
227
  /**
@@ -285,17 +234,57 @@ class Logger {
285
234
  */
286
235
  configure(config = {}) {
287
236
  if (!config) return;
288
- if (config.prettyObjects) this.prettyObjects = config.prettyObjects;
237
+ if (config.prettyObjects === false || config.prettyObjects === true) this.prettyObjects = config.prettyObjects;
289
238
  if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
290
239
  }
291
240
  }
292
241
 
293
242
  Logger.instance = null;
294
243
 
295
- // module.exports.Logger = Logger;
296
244
  const logger = new Logger();
297
245
 
298
246
  module.exports = logger;
299
247
 
300
248
  // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
301
249
  // upd: did not face such loggers, but still could be useful
250
+
251
+ /* Cypress
252
+ There is no listener like "after:test" in cypress, only "after:spec" is available.
253
+ Thus, cannot separate logs even when I gather them (because I don't know when the test is done, just know about suite).
254
+ Also there is no easy way to access the message from cy.log() function.
255
+ (Using testomatio logger – logger.log() is not convenient because Cypress chains its commands,
256
+ thus such command will interrupt the chain.)
257
+
258
+ (Could not implement intercepting of cy.log('message'));
259
+ I found the only ability to get any logs using .task('log', 'message') (this is custom, not default cypress command)
260
+ and then intercept it with:
261
+ on('task', {
262
+ log (message) {
263
+ console.log(message)
264
+ return null
265
+ }
266
+ })
267
+
268
+ but:
269
+ 1) it does not solve problem with getting current running testId;
270
+ 2) leads to warning "Warning: Multiple attempts to register the following task(s):".)
271
+
272
+ My way to get test id:
273
+ add cypress command to save test title to file)):
274
+ Cypress.Commands.add('writeTestTitleToFile', () => {
275
+ const testTitle = cy.state('runnable').title;
276
+ cy.writeFile('testomatio_test_title', testTitle);
277
+ });
278
+
279
+ Finally, in the test it will look like:
280
+ cy
281
+ .writeTestTitleToFile() // <<<
282
+ .task('log', 'This is a log message from the test') // <<<
283
+
284
+ .get('element)
285
+ .type('text')
286
+ .click()
287
+
288
+
289
+ Parallelization in Cypress is only available if using Cypress Dashboard??
290
+ */
@@ -0,0 +1,135 @@
1
+ const { resetConfig } = require('../fileUploader');
2
+ const { APP_PREFIX } = require('../constants');
3
+
4
+ /**
5
+ * Set S3 credentials from the provided artifacts object.
6
+ * @param {Object} artifacts - The artifacts object containing S3 credentials.
7
+ */
8
+ function setS3Credentials(artifacts) {
9
+ if (!Object.keys(artifacts).length) return;
10
+
11
+ console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
12
+
13
+ if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
14
+ if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
15
+ if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
16
+ if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
17
+ if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
18
+ if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
19
+ resetConfig();
20
+ }
21
+ /**
22
+ * Generates mode request parameters based on the input params.
23
+ * @param {Object} params - The input parameters for the request.
24
+ * @param {string} params.type - The type of the request (e.g., "tag").
25
+ * @param {string} params.id - The ID associated with the request.
26
+ * @param {string} params.apiKey - The API key for authentication.
27
+ * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
28
+ */
29
+ function generateFilterRequestParams(params) {
30
+ const { type, id, apiKey } = params;
31
+
32
+ if (!type) {
33
+ return;
34
+ }
35
+
36
+ if (!id) {
37
+ console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
38
+ return;
39
+ }
40
+
41
+ return {
42
+ params: {
43
+ type,
44
+ id: encodeURIComponent(id),
45
+ api_key: apiKey
46
+ },
47
+ responseType: "json"
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Parse filter parameters from a string in the format "type=id".
53
+ * @param {string} opts - The input string containing the filter parameters.
54
+ * @returns {Object} An object containing the parsed filter parameters.
55
+ * The object has properties "type" and "id".
56
+ */
57
+ function parseFilterParams(opts) {
58
+ const [type, id] = opts.split("=");
59
+ const validType = updateFilterType(type);
60
+
61
+ return {
62
+ type: validType,
63
+ id
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Update and validate the filter type.
69
+ * @param {string} type - The original filter type.
70
+ * @returns {string|undefined} The updated and validated filter type.
71
+ * Returns undefined if the type is not valid.
72
+ */
73
+ function updateFilterType(type) {
74
+ const typeLowerCase = type.toLowerCase();
75
+
76
+ const filterTypes = [
77
+ "tag-name",
78
+ "plan-id",
79
+ "label",
80
+ "jira-ticket",
81
+ ];
82
+
83
+ const filterApi = [
84
+ "tag",
85
+ "plan",
86
+ "label",
87
+ "jira",
88
+ // "ims-issue", //TODO: WIP
89
+ ];
90
+
91
+ if (!filterTypes.includes(typeLowerCase)) {
92
+ console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
93
+ return;
94
+ }
95
+
96
+ const index = filterTypes.indexOf(typeLowerCase);
97
+
98
+ return (index !== -1)
99
+ ? filterApi[index]
100
+ : undefined
101
+ }
102
+
103
+ /**
104
+ * Return an emoji based on the provided status.
105
+ * @param {string} status - The status value ('passed', 'failed', or 'skipped').
106
+ * @returns {string} - An emoji corresponding to the provided status.
107
+ */
108
+ function statusEmoji(status) {
109
+ if (status === 'passed') return '🟢';
110
+ if (status === 'failed') return '🔴';
111
+ if (status === 'skipped') return '🟡';
112
+ return '';
113
+ }
114
+
115
+ /**
116
+ * Generate a full name string based on the provided test object.
117
+ * @param {object} t - The test object.
118
+ * @returns {string} - A formatted full name string for the test object.
119
+ */
120
+ function fullName(t) {
121
+ let line = '';
122
+ if (t.suite_title) line = `${t.suite_title}: `;
123
+ line += `**${t.title}**`;
124
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
125
+ return line;
126
+ }
127
+
128
+ module.exports = {
129
+ updateFilterType,
130
+ parseFilterParams,
131
+ generateFilterRequestParams,
132
+ setS3Credentials,
133
+ statusEmoji,
134
+ fullName
135
+ };
@@ -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
  }
@@ -55,11 +55,21 @@ const isValidUrl = s => {
55
55
  }
56
56
  };
57
57
 
58
+ const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/ig;
59
+
58
60
  const fetchFilesFromStackTrace = (stack = '') => {
59
- const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
60
- return Array.from(files)
61
- .map(f => f[1])
62
- .filter(f => fs.existsSync(f));
61
+
62
+ const files = Array.from(stack.matchAll(fileMatchRegex))
63
+ .map(f => f[1].trim())
64
+ .map(f => f.startsWith('//') ? f.substring(1) : f )
65
+
66
+ debug('Found files in stack trace: ', files);
67
+
68
+ return files.filter(f => {
69
+ const isFile = fs.existsSync(f);
70
+ if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
71
+ return isFile;
72
+ });
63
73
  };
64
74
 
65
75
  const fetchSourceCodeFromStackTrace = (stack = '') => {
@@ -87,13 +97,38 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
87
97
 
88
98
  if (!source) return '';
89
99
 
90
- return source.split('\n')
100
+ return source
101
+ .split('\n')
91
102
  .map((l, i) => {
92
103
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
93
- return `${line - prepend + i} | ${l}`
94
- }).join('\n');
104
+ return `${line - prepend + i} | ${l}`;
105
+ })
106
+ .join('\n');
95
107
  };
96
108
 
109
+ const TEST_ID_REGEX = /@T([\w\d]{8})/;
110
+
111
+ const fetchIdFromCode = (code, opts = {}) => {
112
+ const comments = code.split('\n').map(l => l.trim()).filter(l => {
113
+ switch (opts.lang) {
114
+ case 'ruby':
115
+ case 'python':
116
+ return l.startsWith('# ');
117
+ default:
118
+ return l.startsWith('// ');
119
+ }
120
+ });
121
+
122
+ return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
123
+ }
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
+
97
132
  const fetchSourceCode = (contents, opts = {}) => {
98
133
  if (!opts.title && !opts.line) return '';
99
134
 
@@ -115,7 +150,7 @@ const fetchSourceCode = (contents, opts = {}) => {
115
150
 
116
151
  if (lineIndex) {
117
152
  const result = [];
118
- for (let i = lineIndex; i < (lineIndex + limit); i++) {
153
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
119
154
  if (lines[i] === undefined) continue;
120
155
 
121
156
  if (i > lineIndex + 2 && !opts.prepend) {
@@ -189,19 +224,101 @@ const fileSystem = {
189
224
  } else {
190
225
  debug(`Trying to delete ${dirPath} but it doesn't exist`);
191
226
  }
192
- }
227
+ },
228
+ };
229
+
230
+
231
+ const foundedTestLog = (app, tests) => {
232
+ const n = tests.length;
233
+
234
+ return (n === 1)
235
+ ? console.log(app, `✅ We found one test!`)
236
+ : console.log(app, `✅ We found ${n} tests!`);
237
+ }
238
+
239
+ const humanize = text => {
240
+ text = decamelize(text);
241
+ return text
242
+ .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
243
+ .trim()
244
+ .replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
245
+ .trim()
246
+ .replace(/\sA\s/g, ' a ') // replace a|the
247
+ .replace(/\sThe\s/g, ' the ') // replace a|the
248
+ .replace(/^Test\s/, '')
249
+ .replace(/^Should\s/, '');
250
+ };
251
+
252
+ /**
253
+ * From https://github.com/sindresorhus/decamelize/blob/main/index.js
254
+ * @param {*} text
255
+ * @returns
256
+ */
257
+ const decamelize = text => {
258
+ const separator = '_';
259
+ const replacement = `$1${separator}$2`;
260
+
261
+ // Split lowercase sequences followed by uppercase character.
262
+ // `dataForUSACounties` → `data_For_USACounties`
263
+ // `myURLstring → `my_URLstring`
264
+ let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
265
+
266
+ // Lowercase all single uppercase characters. As we
267
+ // want to preserve uppercase sequences, we cannot
268
+ // simply lowercase the separated string at the end.
269
+ // `data_For_USACounties` → `data_for_USACounties`
270
+ decamelized = decamelized.replace(
271
+ /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
272
+ $0 => $0.toLowerCase(),
273
+ );
274
+
275
+ // Remaining uppercase sequences will be separated from lowercase sequences.
276
+ // `data_For_USACounties` → `data_for_USA_counties`
277
+ return decamelized.replace(
278
+ /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
279
+ (_, $1, $2) => $1 + separator + $2.toLowerCase(),
280
+ );
281
+ };
282
+
283
+ /**
284
+ * Used to remove color codes
285
+ * @param {*} input
286
+ * @returns
287
+ */
288
+ function removeColorCodes(input) {
289
+ // eslint-disable-next-line no-control-regex
290
+ return input.replace(/\x1b\[[0-9;]*m/g, '');
291
+ }
292
+
293
+ const jestHelpers = {
294
+ getIdOfCurrentlyRunningTest: () => {
295
+ if (!process.env.JEST_WORKER_ID) return null;
296
+ try {
297
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
298
+ // eslint-disable-next-line no-undef
299
+ if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
300
+ } catch (e) {
301
+ return null;
302
+ }
303
+ },
193
304
  };
194
305
 
195
306
  module.exports = {
196
307
  isSameTest,
197
308
  fetchSourceCode,
198
309
  fetchSourceCodeFromStackTrace,
310
+ fetchIdFromCode,
311
+ fetchIdFromOutput,
199
312
  fetchFilesFromStackTrace,
200
313
  fileSystem,
201
314
  getCurrentDateTime,
315
+ jestHelpers,
202
316
  specificTestInfo,
203
317
  isValidUrl,
204
318
  ansiRegExp,
205
319
  parseTest,
206
320
  parseSuite,
207
- };
321
+ humanize,
322
+ removeColorCodes,
323
+ foundedTestLog
324
+ };