@testomatio/reporter 2.3.8 → 2.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapter/codecept.js +12 -9
- package/lib/client.d.ts +0 -10
- package/lib/client.js +14 -126
- package/lib/junit-adapter/csharp.js +4 -1
- package/lib/junit-adapter/nunit-parser.js +4 -2
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/testomatio.js +17 -13
- package/lib/reporter-functions.js +1 -3
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/xmlReader.js +14 -4
- package/package.json +2 -2
- package/src/adapter/codecept.js +19 -19
- package/src/adapter/mocha.js +1 -1
- package/src/adapter/playwright.js +2 -2
- package/src/bin/cli.js +1 -1
- package/src/client.js +15 -112
- package/src/junit-adapter/csharp.js +4 -1
- package/src/junit-adapter/nunit-parser.js +9 -7
- package/src/pipe/bitbucket.js +5 -5
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/testomatio.js +73 -79
- package/src/reporter-functions.js +2 -3
- package/src/reporter.js +1 -2
- package/src/services/links.js +1 -1
- package/src/utils/log-formatter.js +113 -0
- package/src/xmlReader.js +14 -4
package/src/pipe/testomatio.js
CHANGED
|
@@ -60,8 +60,8 @@ class TestomatioPipe {
|
|
|
60
60
|
retryConfig: {
|
|
61
61
|
retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
62
62
|
retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
63
|
-
httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
|
|
64
|
-
shouldRetry:
|
|
63
|
+
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
64
|
+
shouldRetry: error => {
|
|
65
65
|
if (!error.response) return false;
|
|
66
66
|
switch (error.response?.status) {
|
|
67
67
|
case 400: // Bad request (probably wrong API key)
|
|
@@ -73,8 +73,8 @@ class TestomatioPipe {
|
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
76
|
-
}
|
|
77
|
-
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
this.isEnabled = true;
|
|
@@ -104,7 +104,6 @@ class TestomatioPipe {
|
|
|
104
104
|
// add test ID + run ID
|
|
105
105
|
if (data.rid) data.rid = `${this.runId}-${data.rid}`;
|
|
106
106
|
|
|
107
|
-
|
|
108
107
|
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
109
108
|
data.stack = null;
|
|
110
109
|
}
|
|
@@ -120,7 +119,6 @@ class TestomatioPipe {
|
|
|
120
119
|
return data;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
|
|
124
122
|
/**
|
|
125
123
|
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
126
124
|
* @param {Object} opts - The options for preparing the test grepList.
|
|
@@ -216,7 +214,7 @@ class TestomatioPipe {
|
|
|
216
214
|
method: 'PUT',
|
|
217
215
|
url: `/api/reporter/${this.runId}`,
|
|
218
216
|
data: runParams,
|
|
219
|
-
responseType: 'json'
|
|
217
|
+
responseType: 'json',
|
|
220
218
|
});
|
|
221
219
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
222
220
|
return;
|
|
@@ -229,7 +227,7 @@ class TestomatioPipe {
|
|
|
229
227
|
url: '/api/reporter',
|
|
230
228
|
data: runParams,
|
|
231
229
|
maxContentLength: Infinity,
|
|
232
|
-
responseType: 'json'
|
|
230
|
+
responseType: 'json',
|
|
233
231
|
});
|
|
234
232
|
|
|
235
233
|
this.runId = resp.data.uid;
|
|
@@ -288,44 +286,44 @@ class TestomatioPipe {
|
|
|
288
286
|
|
|
289
287
|
debug('Adding test', json);
|
|
290
288
|
|
|
291
|
-
return this.client
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
289
|
+
return this.client
|
|
290
|
+
.request({
|
|
291
|
+
method: 'POST',
|
|
292
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
293
|
+
data: json,
|
|
294
|
+
headers: {
|
|
295
|
+
'Content-Type': 'application/json',
|
|
296
|
+
},
|
|
297
|
+
maxContentLength: Infinity,
|
|
298
|
+
})
|
|
299
|
+
.catch(err => {
|
|
300
|
+
this.requestFailures++;
|
|
301
|
+
this.notReportedTestsCount++;
|
|
302
|
+
if (err.response) {
|
|
303
|
+
if (err.response.status >= 400) {
|
|
304
|
+
const responseData = err.response.data || { message: '' };
|
|
305
|
+
console.log(
|
|
306
|
+
APP_PREFIX,
|
|
307
|
+
pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
308
|
+
pc.gray(data?.title || ''),
|
|
309
|
+
);
|
|
310
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
311
|
+
this.hasUnmatchedTests = true;
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
305
315
|
console.log(
|
|
306
316
|
APP_PREFIX,
|
|
307
|
-
pc.yellow(`Warning: ${
|
|
308
|
-
|
|
317
|
+
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
318
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
309
319
|
);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return;
|
|
320
|
+
printCreateIssue(err);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
314
323
|
}
|
|
315
|
-
|
|
316
|
-
APP_PREFIX,
|
|
317
|
-
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
318
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
319
|
-
);
|
|
320
|
-
printCreateIssue(err);
|
|
321
|
-
} else {
|
|
322
|
-
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
323
|
-
}
|
|
324
|
-
});
|
|
324
|
+
});
|
|
325
325
|
};
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
327
|
/**
|
|
330
328
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
331
329
|
*/
|
|
@@ -350,43 +348,42 @@ class TestomatioPipe {
|
|
|
350
348
|
const testsToSend = this.batch.tests.splice(0);
|
|
351
349
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
352
350
|
|
|
353
|
-
return this.client
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
351
|
+
return this.client
|
|
352
|
+
.request({
|
|
353
|
+
method: 'POST',
|
|
354
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
355
|
+
data: {
|
|
356
|
+
api_key: this.apiKey,
|
|
357
|
+
tests: testsToSend,
|
|
358
|
+
batch_index: this.batch.batchIndex,
|
|
359
|
+
},
|
|
360
|
+
headers: {
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
},
|
|
363
|
+
maxContentLength: Infinity,
|
|
364
|
+
})
|
|
365
|
+
.catch(err => {
|
|
366
|
+
this.requestFailures++;
|
|
367
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
368
|
+
if (err.response) {
|
|
369
|
+
if (err.response.status >= 400) {
|
|
370
|
+
const responseData = err.response.data || { message: '' };
|
|
371
|
+
console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
|
|
372
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
373
|
+
this.hasUnmatchedTests = true;
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
371
377
|
console.log(
|
|
372
378
|
APP_PREFIX,
|
|
373
|
-
pc.yellow(`Warning:
|
|
379
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
380
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
374
381
|
);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return;
|
|
382
|
+
printCreateIssue(err);
|
|
383
|
+
} else {
|
|
384
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
379
385
|
}
|
|
380
|
-
|
|
381
|
-
APP_PREFIX,
|
|
382
|
-
pc.yellow(`Warning: (${err.response?.status})`),
|
|
383
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
384
|
-
);
|
|
385
|
-
printCreateIssue(err);
|
|
386
|
-
} else {
|
|
387
|
-
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
386
|
+
});
|
|
390
387
|
};
|
|
391
388
|
|
|
392
389
|
/**
|
|
@@ -409,9 +406,9 @@ class TestomatioPipe {
|
|
|
409
406
|
else this.batch.tests.push(data);
|
|
410
407
|
|
|
411
408
|
// if test is added after run which is already finished
|
|
412
|
-
|
|
409
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
413
410
|
|
|
414
|
-
|
|
411
|
+
// return promise to be able to wait for it
|
|
415
412
|
return uploading;
|
|
416
413
|
}
|
|
417
414
|
|
|
@@ -460,7 +457,7 @@ class TestomatioPipe {
|
|
|
460
457
|
status_event,
|
|
461
458
|
detach: params.detach,
|
|
462
459
|
tests: params.tests,
|
|
463
|
-
}
|
|
460
|
+
},
|
|
464
461
|
});
|
|
465
462
|
|
|
466
463
|
if (this.runUrl) {
|
|
@@ -526,9 +523,6 @@ function printCreateIssue(err) {
|
|
|
526
523
|
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
527
524
|
console.log('```');
|
|
528
525
|
});
|
|
529
|
-
|
|
530
526
|
}
|
|
531
527
|
|
|
532
|
-
|
|
533
|
-
|
|
534
528
|
export default TestomatioPipe;
|
|
@@ -62,9 +62,8 @@ function setLabel(key, value = null) {
|
|
|
62
62
|
if (Array.isArray(value)) {
|
|
63
63
|
return value.forEach(label => setLabel(key, label));
|
|
64
64
|
}
|
|
65
|
-
const labelObject =
|
|
66
|
-
? { label: `${key}:${value}` }
|
|
67
|
-
: { label: key };
|
|
65
|
+
const labelObject =
|
|
66
|
+
value !== null && value !== undefined && value !== '' ? { label: `${key}:${value}` } : { label: key };
|
|
68
67
|
services.links.put([labelObject]);
|
|
69
68
|
}
|
|
70
69
|
|
package/src/reporter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Client from './client.js';
|
|
2
|
-
import * as TestomatioConstants
|
|
2
|
+
import * as TestomatioConstants from './constants.js';
|
|
3
3
|
import { services } from './services/index.js';
|
|
4
4
|
import reporterFunctions from './reporter-functions.js';
|
|
5
5
|
|
|
@@ -39,5 +39,4 @@ export default {
|
|
|
39
39
|
|
|
40
40
|
TestomatioClient: Client,
|
|
41
41
|
STATUS,
|
|
42
|
-
|
|
43
42
|
};
|
package/src/services/links.js
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import createCallsiteRecord from 'callsite-record';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { stripVTControlCharacters } from 'util';
|
|
5
|
+
import { sep } from 'path';
|
|
6
|
+
import { formatStep, truncate } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const stripColors = stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
12
|
+
* @param {Object} params - Parameters for formatting logs
|
|
13
|
+
* @param {string} params.error - Error message
|
|
14
|
+
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
15
|
+
* @param {string} params.logs - Test logs
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function formatLogs({ error, steps, logs }) {
|
|
19
|
+
error = error?.trim();
|
|
20
|
+
logs = logs
|
|
21
|
+
?.trim()
|
|
22
|
+
.split('\n')
|
|
23
|
+
.map(l => truncate(l))
|
|
24
|
+
.join('\n');
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(steps)) {
|
|
27
|
+
steps = steps
|
|
28
|
+
.map(step => formatStep(step))
|
|
29
|
+
.flat()
|
|
30
|
+
.join('\n');
|
|
31
|
+
} else {
|
|
32
|
+
steps = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let testLogs = '';
|
|
36
|
+
if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
|
|
37
|
+
if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
|
|
38
|
+
if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
|
|
39
|
+
return testLogs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats an error with stack trace and diff information
|
|
44
|
+
* @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
|
|
45
|
+
* The error object to format
|
|
46
|
+
* @param {string} [message] - Optional error message override
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function formatError(error, message) {
|
|
50
|
+
if (!message) message = error.message;
|
|
51
|
+
// @ts-ignore - inspect is a custom property added by some testing frameworks
|
|
52
|
+
if (error.inspect) message = error.inspect() || '';
|
|
53
|
+
|
|
54
|
+
let stack = '';
|
|
55
|
+
if (error.name) stack += `${pc.red(error.name)}`;
|
|
56
|
+
// @ts-ignore - operator is a custom property added by assertion libraries
|
|
57
|
+
if (error.operator) stack += ` (${pc.red(error.operator)})`;
|
|
58
|
+
// add new line if something was added to stack
|
|
59
|
+
if (stack) stack += ': ';
|
|
60
|
+
|
|
61
|
+
stack += `${message}\n`;
|
|
62
|
+
|
|
63
|
+
// @ts-ignore - diff is a custom property added by vitest
|
|
64
|
+
if (error.diff) {
|
|
65
|
+
// diff for vitest
|
|
66
|
+
stack += error.diff;
|
|
67
|
+
stack += '\n\n';
|
|
68
|
+
} else if (error.actual && error.expected && error.actual !== error.expected) {
|
|
69
|
+
// diffs for mocha, cypress, codeceptjs style
|
|
70
|
+
stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
|
|
71
|
+
stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
|
|
72
|
+
stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
|
|
73
|
+
stack += '\n\n';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
let hasFrame = false;
|
|
80
|
+
const record = createCallsiteRecord({
|
|
81
|
+
forError: error,
|
|
82
|
+
isCallsiteFrame: frame => {
|
|
83
|
+
if (customFilter && minimatch(frame.fileName, customFilter)) return false;
|
|
84
|
+
if (hasFrame) return false;
|
|
85
|
+
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
86
|
+
return hasFrame;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
if (record && !record.filename.startsWith('http')) {
|
|
91
|
+
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
92
|
+
}
|
|
93
|
+
return stack;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.log(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks if a stack frame is not an internal frame (node_modules or internal)
|
|
101
|
+
* @param {Object} frame - Stack frame object
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isNotInternalFrame(frame) {
|
|
105
|
+
return (
|
|
106
|
+
frame.getFileName() &&
|
|
107
|
+
frame.getFileName().includes(sep) &&
|
|
108
|
+
!frame.getFileName().includes('node_modules') &&
|
|
109
|
+
!frame.getFileName().includes('internal')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { stripColors };
|
package/src/xmlReader.js
CHANGED
|
@@ -274,9 +274,14 @@ class XmlReader {
|
|
|
274
274
|
_parseTRXTestDefinition(td) {
|
|
275
275
|
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
276
276
|
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
277
|
-
const example = exampleMatch
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
const example = exampleMatch
|
|
278
|
+
? {
|
|
279
|
+
...exampleMatch[1]
|
|
280
|
+
.split(',')
|
|
281
|
+
.map(p => p.trim())
|
|
282
|
+
.filter(p => p !== ''),
|
|
283
|
+
}
|
|
284
|
+
: null;
|
|
280
285
|
|
|
281
286
|
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
282
287
|
const suite_title = suite.pop();
|
|
@@ -600,7 +605,12 @@ function reduceTestCases(prev, item) {
|
|
|
600
605
|
|
|
601
606
|
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
602
607
|
if (exampleMatches) {
|
|
603
|
-
example = {
|
|
608
|
+
example = {
|
|
609
|
+
...exampleMatches[1]
|
|
610
|
+
.split(',')
|
|
611
|
+
.map(v => v.trim().replace(/[^\w\s-]/g, ''))
|
|
612
|
+
.filter(v => v !== ''),
|
|
613
|
+
};
|
|
604
614
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
605
615
|
}
|
|
606
616
|
|