@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.
- package/README.md +4 -4
- package/lib/adapter/codecept.js +11 -9
- package/lib/adapter/cucumber/current.js +4 -4
- package/lib/adapter/cucumber/legacy.js +3 -3
- package/lib/adapter/cypress-plugin/index.js +1 -1
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +12 -17
- package/lib/adapter/mocha.js +22 -50
- package/lib/adapter/playwright.js +34 -22
- package/lib/adapter/webdriver.js +1 -1
- package/lib/bin/reportXml.js +1 -0
- package/lib/bin/startTest.js +25 -4
- package/lib/client.js +93 -27
- package/lib/constants.js +4 -6
- package/lib/fileUploader.js +106 -38
- package/lib/junit-adapter/java.js +27 -9
- package/lib/pipe/csv.js +4 -1
- package/lib/pipe/github.js +5 -16
- package/lib/pipe/gitlab.js +5 -16
- package/lib/pipe/testomatio.js +136 -34
- package/lib/reporter-functions.js +43 -0
- package/lib/reporter.js +11 -5
- package/lib/storages/artifact-storage.js +70 -0
- package/lib/storages/data-storage.js +307 -0
- package/lib/storages/key-value-storage.js +58 -0
- package/lib/{logger.js → storages/logger.js} +103 -114
- package/lib/utils/pipe_utils.js +135 -0
- package/lib/{util.js → utils/utils.js} +129 -12
- package/lib/xmlReader.js +132 -44
- package/package.json +9 -7
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -244
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const debug = require('debug')('@testomatio/reporter:logger');
|
|
3
|
-
const
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* 3. Varied syntax.
|
|
21
|
+
* Logger allows to intercept logs from any logger (console.log, tracer, pino, etc)
|
|
22
|
+
* and save in the testomatio reporter.
|
|
23
|
+
* Supports different syntaxes to satisfy any user preferences.
|
|
23
24
|
*/
|
|
24
25
|
class Logger {
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
constructor() {
|
|
29
|
-
// set default logger to be used in log, warn, error, etc methods
|
|
30
|
-
this._originalUserLogger = { ...console };
|
|
26
|
+
// set default logger to be used in log, warn, error, etc methods
|
|
27
|
+
#originalUserLogger = { ...console };
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
29
|
+
#dataStorage = new DataStorage('log');
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
// if (params?.logger) this._loggerToIntercept = params.logger;
|
|
31
|
+
logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
|
|
37
32
|
|
|
33
|
+
constructor() {
|
|
38
34
|
// intercept console by default
|
|
39
35
|
this.intercept(console);
|
|
40
36
|
|
|
@@ -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
|
|
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
|
|
67
|
+
const logs = this.#dataStorage.getData(context);
|
|
83
68
|
return logs || '';
|
|
84
69
|
}
|
|
85
70
|
|
|
86
|
-
|
|
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:
|
|
112
|
-
* 2. Standard:
|
|
113
|
-
* 3. Standard with multiple arguments:
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
141
|
-
: '
|
|
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
|
|
124
|
+
logs = this.#stringifyLogs(strings, ...args);
|
|
149
125
|
}
|
|
150
|
-
this.
|
|
151
|
-
this
|
|
126
|
+
this.#originalUserLogger.log(logs);
|
|
127
|
+
this.#dataStorage.putData(logs);
|
|
152
128
|
}
|
|
153
129
|
|
|
154
130
|
/**
|
|
155
|
-
* This function is a wrapper for
|
|
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
|
-
|
|
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
|
|
142
|
+
const logs = this.#stringifyLogs(...argsArray);
|
|
176
143
|
const colorizedLogs = chalk[LEVELS[level].color](logs);
|
|
177
|
-
this
|
|
144
|
+
this.#dataStorage.putData(colorizedLogs);
|
|
178
145
|
try {
|
|
179
146
|
// level.toLowerCase() represents method name (log, warn, error, etc)
|
|
180
|
-
this
|
|
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
|
-
|
|
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
|
|
158
|
+
this.#logWrapper(args, 'DEBUG');
|
|
205
159
|
}
|
|
206
160
|
|
|
207
161
|
error(...args) {
|
|
208
|
-
this
|
|
162
|
+
this.#logWrapper(args, 'ERROR');
|
|
209
163
|
}
|
|
210
164
|
|
|
211
165
|
info(...args) {
|
|
212
|
-
this
|
|
166
|
+
this.#logWrapper(args, 'INFO');
|
|
213
167
|
}
|
|
214
168
|
|
|
215
169
|
log(...args) {
|
|
216
|
-
this
|
|
170
|
+
this.#logWrapper(args, 'LOG');
|
|
217
171
|
}
|
|
218
172
|
|
|
219
173
|
trace(...args) {
|
|
220
|
-
this
|
|
174
|
+
this.#logWrapper(args, 'TRACE');
|
|
221
175
|
}
|
|
222
176
|
|
|
223
177
|
warn(...args) {
|
|
224
|
-
this
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
})
|
|
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 <
|
|
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
|
+
};
|