@testomatio/reporter 2.3.9-beta-bin-fix → 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/README.md +2 -1
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +14 -4
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +39 -142
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +43 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +433 -0
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/testomatio.d.ts +2 -1
- package/lib/pipe/testomatio.js +19 -14
- package/lib/reporter-functions.js +1 -3
- package/lib/reporter.d.ts +19 -9
- package/lib/reporter.js +40 -5
- package/lib/uploader.js +4 -0
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/utils/utils.js +189 -24
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +8 -4
- 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 +16 -4
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +47 -116
- package/src/junit-adapter/csharp.js +48 -6
- package/src/junit-adapter/nunit-parser.js +474 -0
- 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 +75 -80
- package/src/reporter-functions.js +2 -3
- package/src/reporter.js +6 -4
- package/src/services/links.js +1 -1
- package/src/uploader.js +5 -0
- package/src/utils/log-formatter.js +113 -0
- package/src/utils/utils.js +202 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
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.
|
|
@@ -163,7 +161,7 @@ class TestomatioPipe {
|
|
|
163
161
|
|
|
164
162
|
/**
|
|
165
163
|
* Creates a new run on Testomat.io
|
|
166
|
-
* @param {{isBatchEnabled?: boolean}} params
|
|
164
|
+
* @param {{isBatchEnabled?: boolean, kind?: string}} params
|
|
167
165
|
* @returns Promise<void>
|
|
168
166
|
*/
|
|
169
167
|
async createRun(params = {}) {
|
|
@@ -204,6 +202,7 @@ class TestomatioPipe {
|
|
|
204
202
|
label: this.label,
|
|
205
203
|
shared_run: this.sharedRun,
|
|
206
204
|
shared_run_timeout: this.sharedRunTimeout,
|
|
205
|
+
kind: params.kind,
|
|
207
206
|
}).filter(([, value]) => !!value),
|
|
208
207
|
);
|
|
209
208
|
debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
|
|
@@ -215,7 +214,7 @@ class TestomatioPipe {
|
|
|
215
214
|
method: 'PUT',
|
|
216
215
|
url: `/api/reporter/${this.runId}`,
|
|
217
216
|
data: runParams,
|
|
218
|
-
responseType: 'json'
|
|
217
|
+
responseType: 'json',
|
|
219
218
|
});
|
|
220
219
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
221
220
|
return;
|
|
@@ -228,7 +227,7 @@ class TestomatioPipe {
|
|
|
228
227
|
url: '/api/reporter',
|
|
229
228
|
data: runParams,
|
|
230
229
|
maxContentLength: Infinity,
|
|
231
|
-
responseType: 'json'
|
|
230
|
+
responseType: 'json',
|
|
232
231
|
});
|
|
233
232
|
|
|
234
233
|
this.runId = resp.data.uid;
|
|
@@ -287,44 +286,44 @@ class TestomatioPipe {
|
|
|
287
286
|
|
|
288
287
|
debug('Adding test', json);
|
|
289
288
|
|
|
290
|
-
return this.client
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
}
|
|
304
315
|
console.log(
|
|
305
316
|
APP_PREFIX,
|
|
306
|
-
pc.yellow(`Warning: ${
|
|
307
|
-
|
|
317
|
+
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
318
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
308
319
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return;
|
|
320
|
+
printCreateIssue(err);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
313
323
|
}
|
|
314
|
-
|
|
315
|
-
APP_PREFIX,
|
|
316
|
-
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
317
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
318
|
-
);
|
|
319
|
-
printCreateIssue(err);
|
|
320
|
-
} else {
|
|
321
|
-
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
+
});
|
|
324
325
|
};
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
327
|
/**
|
|
329
328
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
330
329
|
*/
|
|
@@ -349,43 +348,42 @@ class TestomatioPipe {
|
|
|
349
348
|
const testsToSend = this.batch.tests.splice(0);
|
|
350
349
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
351
350
|
|
|
352
|
-
return this.client
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
377
|
console.log(
|
|
371
378
|
APP_PREFIX,
|
|
372
|
-
pc.yellow(`Warning:
|
|
379
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
380
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
373
381
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return;
|
|
382
|
+
printCreateIssue(err);
|
|
383
|
+
} else {
|
|
384
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
378
385
|
}
|
|
379
|
-
|
|
380
|
-
APP_PREFIX,
|
|
381
|
-
pc.yellow(`Warning: (${err.response?.status})`),
|
|
382
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
383
|
-
);
|
|
384
|
-
printCreateIssue(err);
|
|
385
|
-
} else {
|
|
386
|
-
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
386
|
+
});
|
|
389
387
|
};
|
|
390
388
|
|
|
391
389
|
/**
|
|
@@ -408,9 +406,9 @@ class TestomatioPipe {
|
|
|
408
406
|
else this.batch.tests.push(data);
|
|
409
407
|
|
|
410
408
|
// if test is added after run which is already finished
|
|
411
|
-
|
|
409
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
412
410
|
|
|
413
|
-
|
|
411
|
+
// return promise to be able to wait for it
|
|
414
412
|
return uploading;
|
|
415
413
|
}
|
|
416
414
|
|
|
@@ -459,7 +457,7 @@ class TestomatioPipe {
|
|
|
459
457
|
status_event,
|
|
460
458
|
detach: params.detach,
|
|
461
459
|
tests: params.tests,
|
|
462
|
-
}
|
|
460
|
+
},
|
|
463
461
|
});
|
|
464
462
|
|
|
465
463
|
if (this.runUrl) {
|
|
@@ -525,9 +523,6 @@ function printCreateIssue(err) {
|
|
|
525
523
|
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
526
524
|
console.log('```');
|
|
527
525
|
});
|
|
528
|
-
|
|
529
526
|
}
|
|
530
527
|
|
|
531
|
-
|
|
532
|
-
|
|
533
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,8 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import Client from './client.js';
|
|
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
|
|
|
6
|
+
export { Client };
|
|
7
|
+
export const STATUS = TestomatioConstants.STATUS;
|
|
6
8
|
export const artifact = reporterFunctions.artifact;
|
|
7
9
|
export const log = reporterFunctions.log;
|
|
8
10
|
export const logger = services.logger;
|
|
@@ -35,6 +37,6 @@ export default {
|
|
|
35
37
|
linkTest: reporterFunctions.linkTest,
|
|
36
38
|
linkJira: reporterFunctions.linkJira,
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
TestomatioClient: Client,
|
|
41
|
+
STATUS,
|
|
40
42
|
};
|
package/src/services/links.js
CHANGED
package/src/uploader.js
CHANGED
|
@@ -194,6 +194,11 @@ export class S3Uploader {
|
|
|
194
194
|
filePath = path.join(process.cwd(), filePath);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Normalize path separators for cross-platform compatibility
|
|
198
|
+
if (typeof filePath === 'string') {
|
|
199
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
200
|
+
}
|
|
201
|
+
|
|
197
202
|
const data = { rid, file: filePath, uploaded };
|
|
198
203
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
199
204
|
fs.appendFileSync(tempFilePath, jsonLine);
|
|
@@ -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 };
|