@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/client.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import createDebugMessages from 'debug';
|
|
2
|
-
import createCallsiteRecord from 'callsite-record';
|
|
3
|
-
import { minimatch } from 'minimatch';
|
|
4
2
|
import fs from 'fs';
|
|
5
3
|
import pc from 'picocolors';
|
|
6
|
-
import { randomUUID } from 'crypto';
|
|
7
4
|
import { APP_PREFIX, STATUS } from './constants.js';
|
|
8
5
|
import { pipesFactory } from './pipe/index.js';
|
|
9
6
|
import { glob } from 'glob';
|
|
10
|
-
import path
|
|
7
|
+
import path from 'path';
|
|
11
8
|
import { fileURLToPath } from 'node:url';
|
|
12
9
|
import { S3Uploader } from './uploader.js';
|
|
13
|
-
import {
|
|
10
|
+
import { readLatestRunId, storeRunId, validateSuiteId, transformEnvVarToBoolean } from './utils/utils.js';
|
|
14
11
|
import { filesize as prettyBytes } from 'filesize';
|
|
12
|
+
import { formatLogs, formatError, stripColors } from './utils/log-formatter.js';
|
|
15
13
|
|
|
16
14
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
17
15
|
|
|
@@ -110,7 +108,7 @@ class Client {
|
|
|
110
108
|
*
|
|
111
109
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
112
110
|
*/
|
|
113
|
-
async createRun(params) {
|
|
111
|
+
async createRun(params = {}) {
|
|
114
112
|
if (!this.pipes || !this.pipes.length)
|
|
115
113
|
this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
116
114
|
debug('Creating run...');
|
|
@@ -118,7 +116,7 @@ class Client {
|
|
|
118
116
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
119
117
|
|
|
120
118
|
this.queue = this.queue
|
|
121
|
-
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
119
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
|
|
122
120
|
.catch(err => console.log(APP_PREFIX, err))
|
|
123
121
|
.then(() => {
|
|
124
122
|
const runId = this.pipeStore?.runId;
|
|
@@ -139,19 +137,6 @@ class Client {
|
|
|
139
137
|
* @returns {Promise<PipeResult[]>}
|
|
140
138
|
*/
|
|
141
139
|
async addTestRun(status, testData) {
|
|
142
|
-
if (!this.pipes || !this.pipes.length)
|
|
143
|
-
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
144
|
-
|
|
145
|
-
// all pipes disabled, skipping
|
|
146
|
-
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
147
|
-
|
|
148
|
-
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
149
|
-
|
|
150
|
-
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
151
|
-
debug('Skipping test from report', testData?.title);
|
|
152
|
-
return []; // do not log skipped tests
|
|
153
|
-
}
|
|
154
|
-
|
|
155
140
|
if (!testData)
|
|
156
141
|
testData = {
|
|
157
142
|
title: 'Unknown test',
|
|
@@ -166,18 +151,19 @@ class Client {
|
|
|
166
151
|
/**
|
|
167
152
|
* @type {TestData}
|
|
168
153
|
*/
|
|
154
|
+
const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
|
|
155
|
+
let steps = originalSteps;
|
|
156
|
+
|
|
157
|
+
const uploadedFiles = [];
|
|
158
|
+
const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
159
|
+
|
|
169
160
|
const {
|
|
170
|
-
rid,
|
|
171
|
-
error = null,
|
|
172
161
|
time = 0,
|
|
173
162
|
example = null,
|
|
174
163
|
files = [],
|
|
175
164
|
filesBuffers = [],
|
|
176
|
-
steps,
|
|
177
165
|
code = null,
|
|
178
|
-
title,
|
|
179
166
|
file,
|
|
180
|
-
suite_title,
|
|
181
167
|
suite_id,
|
|
182
168
|
test_id,
|
|
183
169
|
timestamp,
|
|
@@ -188,7 +174,6 @@ class Client {
|
|
|
188
174
|
} = testData;
|
|
189
175
|
let { message = '', meta = {} } = testData;
|
|
190
176
|
|
|
191
|
-
// stringify meta values and limit keys and values length to 255
|
|
192
177
|
meta = Object.entries(meta)
|
|
193
178
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
194
179
|
.reduce((acc, [key, value]) => {
|
|
@@ -196,22 +181,46 @@ class Client {
|
|
|
196
181
|
return acc;
|
|
197
182
|
}, {});
|
|
198
183
|
|
|
199
|
-
// Get links from storage using the test context
|
|
200
184
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
201
185
|
|
|
202
186
|
let errorFormatted = '';
|
|
203
187
|
if (error) {
|
|
204
|
-
errorFormatted +=
|
|
188
|
+
errorFormatted += formatError(error) || '';
|
|
205
189
|
message = error?.message;
|
|
206
190
|
}
|
|
207
191
|
|
|
208
|
-
|
|
209
|
-
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
192
|
+
let fullLogs = formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
194
|
+
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
195
|
+
uploadedFiles.push(
|
|
196
|
+
this.uploader.uploadFileAsBuffer(Buffer.from(stripColors(fullLogs), 'utf8'), [
|
|
197
|
+
this.runId,
|
|
198
|
+
rid,
|
|
199
|
+
`logs_${+new Date()}.log`,
|
|
200
|
+
]),
|
|
201
|
+
);
|
|
202
|
+
fullLogs = '';
|
|
203
|
+
steps = null;
|
|
204
|
+
}
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
if (!this.pipes || !this.pipes.length)
|
|
207
|
+
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
208
|
+
|
|
209
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) {
|
|
210
|
+
if (uploadedFiles.length > 0) {
|
|
211
|
+
await Promise.all(uploadedFiles);
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isTestShouldBeExcludedFromReport(testData)) return [];
|
|
217
|
+
|
|
218
|
+
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
219
|
+
debug('Skipping test from report', testData?.title);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
215
224
|
|
|
216
225
|
for (let f of files) {
|
|
217
226
|
if (!f) continue; // f === null
|
|
@@ -308,7 +317,7 @@ class Client {
|
|
|
308
317
|
const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
|
|
309
318
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
310
319
|
link: file.link,
|
|
311
|
-
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
320
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
312
321
|
}));
|
|
313
322
|
|
|
314
323
|
uploadedArtifacts.forEach(upload => {
|
|
@@ -330,7 +339,7 @@ class Client {
|
|
|
330
339
|
);
|
|
331
340
|
const failedUploads = this.uploader.failedUploads.map(file => ({
|
|
332
341
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
333
|
-
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
342
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
334
343
|
}));
|
|
335
344
|
|
|
336
345
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
@@ -380,84 +389,6 @@ class Client {
|
|
|
380
389
|
|
|
381
390
|
return this.queue;
|
|
382
391
|
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
386
|
-
* @returns {string}
|
|
387
|
-
*/
|
|
388
|
-
formatLogs({ error, steps, logs }) {
|
|
389
|
-
error = error?.trim();
|
|
390
|
-
logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
|
|
391
|
-
|
|
392
|
-
if (Array.isArray(steps)) {
|
|
393
|
-
steps = steps
|
|
394
|
-
.map(step => formatStep(step))
|
|
395
|
-
.flat()
|
|
396
|
-
.join('\n');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let testLogs = '';
|
|
400
|
-
if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
|
|
401
|
-
if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
|
|
402
|
-
if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
|
|
403
|
-
return testLogs;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
formatError(error, message) {
|
|
407
|
-
if (!message) message = error.message;
|
|
408
|
-
if (error.inspect) message = error.inspect() || '';
|
|
409
|
-
|
|
410
|
-
let stack = '';
|
|
411
|
-
if (error.name) stack += `${pc.red(error.name)}`;
|
|
412
|
-
if (error.operator) stack += ` (${pc.red(error.operator)})`;
|
|
413
|
-
// add new line if something was added to stack
|
|
414
|
-
if (stack) stack += ': ';
|
|
415
|
-
|
|
416
|
-
stack += `${message}\n`;
|
|
417
|
-
|
|
418
|
-
if (error.diff) {
|
|
419
|
-
// diff for vitest
|
|
420
|
-
stack += error.diff;
|
|
421
|
-
stack += '\n\n';
|
|
422
|
-
} else if (error.actual && error.expected && error.actual !== error.expected) {
|
|
423
|
-
// diffs for mocha, cypress, codeceptjs style
|
|
424
|
-
stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
|
|
425
|
-
stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
|
|
426
|
-
stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
|
|
427
|
-
stack += '\n\n';
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
let hasFrame = false;
|
|
434
|
-
const record = createCallsiteRecord({
|
|
435
|
-
forError: error,
|
|
436
|
-
isCallsiteFrame: frame => {
|
|
437
|
-
if (customFilter && minimatch(frame.fileName, customFilter)) return false;
|
|
438
|
-
if (hasFrame) return false;
|
|
439
|
-
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
440
|
-
return hasFrame;
|
|
441
|
-
},
|
|
442
|
-
});
|
|
443
|
-
// @ts-ignore
|
|
444
|
-
if (record && !record.filename.startsWith('http')) {
|
|
445
|
-
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
446
|
-
}
|
|
447
|
-
return stack;
|
|
448
|
-
} catch (e) {
|
|
449
|
-
console.log(e);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function isNotInternalFrame(frame) {
|
|
455
|
-
return (
|
|
456
|
-
frame.getFileName() &&
|
|
457
|
-
frame.getFileName().includes(sep) &&
|
|
458
|
-
!frame.getFileName().includes('node_modules') &&
|
|
459
|
-
!frame.getFileName().includes('internal')
|
|
460
|
-
);
|
|
461
392
|
}
|
|
462
393
|
|
|
463
394
|
/**
|
|
@@ -465,7 +396,7 @@ function isNotInternalFrame(frame) {
|
|
|
465
396
|
* @param {TestData} testData
|
|
466
397
|
* @returns boolean
|
|
467
398
|
*/
|
|
468
|
-
function
|
|
399
|
+
function isTestShouldBeExcludedFromReport(testData) {
|
|
469
400
|
// const fileName = path.basename(test.location?.file || '');
|
|
470
401
|
const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
|
|
471
402
|
if (!globExcludeFilesPattern) return false;
|
|
@@ -475,12 +406,12 @@ function isTestShouldBeExculedFromReport(testData) {
|
|
|
475
406
|
return false;
|
|
476
407
|
}
|
|
477
408
|
|
|
478
|
-
const
|
|
409
|
+
const excludePatternsList = globExcludeFilesPattern.split(';');
|
|
479
410
|
|
|
480
411
|
// as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
|
|
481
412
|
if (!listOfTestFilesToExcludeFromReport) {
|
|
482
413
|
// list of files with relative paths
|
|
483
|
-
listOfTestFilesToExcludeFromReport = glob.sync(
|
|
414
|
+
listOfTestFilesToExcludeFromReport = glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
|
|
484
415
|
debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
|
|
485
416
|
}
|
|
486
417
|
|
|
@@ -3,18 +3,53 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Extract example from title if not already present
|
|
7
|
+
if (!t.example) {
|
|
8
|
+
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
9
|
+
if (exampleMatch) {
|
|
10
|
+
// Extract parameters as object with numeric keys for API
|
|
11
|
+
const params = exampleMatch[1]
|
|
12
|
+
.split(',')
|
|
13
|
+
.map(param => param.trim())
|
|
14
|
+
.filter(param => param !== '');
|
|
15
|
+
t.example = {};
|
|
16
|
+
params.forEach((param, index) => {
|
|
17
|
+
t.example[index] = param;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Remove parameters from title to avoid duplicates in Test Suite
|
|
23
|
+
// The example field will be used for grouping on import
|
|
24
|
+
t.title = t.title.replace(/\(.*?\)/, '').trim();
|
|
25
|
+
|
|
9
26
|
const suite = t.suite_title.split('.');
|
|
10
27
|
t.suite_title = suite.pop();
|
|
11
28
|
t.file = namespaceToFileName(t.file);
|
|
12
|
-
t.title = title.trim();
|
|
13
29
|
return t;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
getFilePath(t) {
|
|
17
|
-
|
|
33
|
+
if (!t.file) return null;
|
|
34
|
+
|
|
35
|
+
// Normalize path separators for cross-platform compatibility
|
|
36
|
+
let filePath = t.file.replace(/\\/g, '/');
|
|
37
|
+
|
|
38
|
+
// If file already has .cs extension, use it directly
|
|
39
|
+
if (filePath.endsWith('.cs')) {
|
|
40
|
+
// Make relative path if it's absolute
|
|
41
|
+
if (path.isAbsolute(filePath)) {
|
|
42
|
+
// Try to find project-relative path
|
|
43
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
44
|
+
if (filePath.startsWith(cwd)) {
|
|
45
|
+
filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Convert namespace path to file path
|
|
52
|
+
const fileName = namespaceToFileName(filePath);
|
|
18
53
|
return fileName;
|
|
19
54
|
}
|
|
20
55
|
}
|
|
@@ -22,7 +57,14 @@ class CSharpAdapter extends Adapter {
|
|
|
22
57
|
export default CSharpAdapter;
|
|
23
58
|
|
|
24
59
|
function namespaceToFileName(fileName) {
|
|
60
|
+
if (!fileName) return '';
|
|
61
|
+
|
|
62
|
+
// If already a .cs file path, clean it up
|
|
63
|
+
if (fileName.endsWith('.cs')) {
|
|
64
|
+
return fileName.replace(/\\/g, '/');
|
|
65
|
+
}
|
|
66
|
+
|
|
25
67
|
const fileParts = fileName.split('.');
|
|
26
68
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
27
|
-
return `${fileParts.join(
|
|
69
|
+
return `${fileParts.join('/')}.cs`;
|
|
28
70
|
}
|