@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-beta.3-xml-import
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 +22 -2
- package/lib/bin/cli.js +1 -1
- package/lib/bin/reportXml.js +1 -1
- package/lib/bin/startTest.js +3 -3
- package/lib/client.js +7 -3
- package/lib/junit-adapter/csharp.js +10 -6
- package/lib/junit-adapter/nunit-parser.js +14 -2
- package/lib/pipe/testomatio.js +1 -2
- package/lib/template/testomatio.hbs +1366 -1026
- package/lib/utils/utils.d.ts +1 -0
- package/lib/utils/utils.js +89 -26
- package/lib/xmlReader.d.ts +21 -0
- package/lib/xmlReader.js +56 -49
- package/package.json +1 -1
- package/src/adapter/codecept.js +27 -3
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +1 -1
- package/src/bin/startTest.js +5 -5
- package/src/client.js +8 -5
- package/src/junit-adapter/csharp.js +11 -6
- package/src/junit-adapter/nunit-parser.js +15 -2
- package/src/pipe/testomatio.js +1 -3
- package/src/template/testomatio.hbs +1366 -1026
- package/src/utils/utils.js +91 -22
- package/src/xmlReader.js +66 -44
package/lib/utils/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export function getPackageVersion(): any;
|
|
|
2
2
|
export const TEST_ID_REGEX: RegExp;
|
|
3
3
|
export const SUITE_ID_REGEX: RegExp;
|
|
4
4
|
export function ansiRegExp(): RegExp;
|
|
5
|
+
export function truncate(s: any, size?: number): any;
|
|
5
6
|
export function cleanLatestRunId(): any;
|
|
6
7
|
export function isSameTest(test: any, t: any): boolean;
|
|
7
8
|
export function fetchSourceCode(contents: any, opts?: {}): string;
|
package/lib/utils/utils.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
|
|
40
40
|
exports.getPackageVersion = getPackageVersion;
|
|
41
|
+
exports.truncate = truncate;
|
|
41
42
|
exports.cleanLatestRunId = cleanLatestRunId;
|
|
42
43
|
exports.formatStep = formatStep;
|
|
43
44
|
exports.readLatestRunId = readLatestRunId;
|
|
@@ -213,27 +214,56 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
213
214
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
214
215
|
}
|
|
215
216
|
else if (opts.lang === 'csharp') {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
217
|
+
// Find the method declaration line
|
|
218
|
+
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
|
219
|
+
if (methodLineIndex === -1) {
|
|
220
|
+
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
220
221
|
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
222
|
+
if (methodLineIndex === -1) {
|
|
223
|
+
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
223
224
|
}
|
|
224
|
-
//
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
226
|
+
if (methodLineIndex !== -1) {
|
|
227
|
+
lineIndex = methodLineIndex;
|
|
228
|
+
// Scan upwards to find the start of attributes and comments
|
|
229
|
+
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
230
|
+
const trimmedLine = lines[i].trim();
|
|
231
|
+
// Include [TestCase], [Test], and other attributes
|
|
232
|
+
if (trimmedLine.startsWith('[')) {
|
|
233
|
+
lineIndex = i;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Include XML documentation comments
|
|
237
|
+
if (trimmedLine.startsWith('///')) {
|
|
238
|
+
lineIndex = i;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Stop at empty lines (with some tolerance)
|
|
242
|
+
if (trimmedLine === '') {
|
|
243
|
+
// Check if next non-empty line is an attribute or comment
|
|
244
|
+
let hasMoreAttributes = false;
|
|
245
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
246
|
+
const nextTrimmed = lines[j].trim();
|
|
247
|
+
if (nextTrimmed === '')
|
|
248
|
+
continue;
|
|
249
|
+
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
250
|
+
hasMoreAttributes = true;
|
|
251
|
+
lineIndex = j;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (!hasMoreAttributes)
|
|
256
|
+
break;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// Stop at other method declarations or class-level elements
|
|
260
|
+
if (trimmedLine.includes('public ') ||
|
|
261
|
+
trimmedLine.includes('private ') ||
|
|
262
|
+
trimmedLine.includes('protected ') ||
|
|
263
|
+
trimmedLine.includes('internal ')) {
|
|
264
|
+
if (!trimmedLine.startsWith('['))
|
|
265
|
+
break;
|
|
232
266
|
}
|
|
233
|
-
return false;
|
|
234
|
-
});
|
|
235
|
-
if (testAttributeIndex !== -1) {
|
|
236
|
-
lineIndex = testAttributeIndex;
|
|
237
267
|
}
|
|
238
268
|
}
|
|
239
269
|
}
|
|
@@ -246,9 +276,26 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
246
276
|
}
|
|
247
277
|
if (lineIndex !== -1 && lineIndex !== undefined) {
|
|
248
278
|
const result = [];
|
|
279
|
+
let braceDepth = 0; // Track brace depth for C# methods
|
|
280
|
+
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
249
281
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
250
282
|
if (lines[i] === undefined)
|
|
251
283
|
continue;
|
|
284
|
+
// Track brace depth for C# to stop after method closes
|
|
285
|
+
if (opts.lang === 'csharp') {
|
|
286
|
+
const line = lines[i];
|
|
287
|
+
// Count opening and closing braces
|
|
288
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
289
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
290
|
+
if (openBraces > 0)
|
|
291
|
+
methodStartFound = true;
|
|
292
|
+
braceDepth += openBraces - closeBraces;
|
|
293
|
+
// If we've started the method and depth returns to 0, method is complete
|
|
294
|
+
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
295
|
+
result.push(lines[i]);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
252
299
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
253
300
|
// annotation
|
|
254
301
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
|
|
@@ -287,14 +334,22 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
287
334
|
break;
|
|
288
335
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
289
336
|
break;
|
|
290
|
-
if
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
337
|
+
// For C#, additional checks if brace tracking didn't stop us
|
|
338
|
+
if (opts.lang === 'csharp') {
|
|
339
|
+
const trimmed = lines[i].trim();
|
|
340
|
+
// Stop at attribute that marks beginning of next test
|
|
341
|
+
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/))
|
|
342
|
+
break;
|
|
343
|
+
// Stop at XML documentation comments that belong to next method
|
|
344
|
+
if (trimmed.startsWith('///'))
|
|
345
|
+
break;
|
|
346
|
+
// Stop at another method declaration
|
|
347
|
+
if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/))
|
|
348
|
+
break;
|
|
349
|
+
// Stop at class declaration
|
|
350
|
+
if (trimmed.includes(' class ') && trimmed.includes('public'))
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
298
353
|
}
|
|
299
354
|
result.push(lines[i]);
|
|
300
355
|
}
|
|
@@ -494,9 +549,17 @@ function transformEnvVarToBoolean(value) {
|
|
|
494
549
|
// if not recognized, return truthy if any value is set
|
|
495
550
|
return Boolean(value);
|
|
496
551
|
}
|
|
552
|
+
function truncate(s, size = 255) {
|
|
553
|
+
if (s.toString().trim().length < size) {
|
|
554
|
+
return s.toString();
|
|
555
|
+
}
|
|
556
|
+
return `${s.toString().substring(0, size)}...`;
|
|
557
|
+
}
|
|
497
558
|
|
|
498
559
|
module.exports.getPackageVersion = getPackageVersion;
|
|
499
560
|
|
|
561
|
+
module.exports.truncate = truncate;
|
|
562
|
+
|
|
500
563
|
module.exports.cleanLatestRunId = cleanLatestRunId;
|
|
501
564
|
|
|
502
565
|
module.exports.formatStep = formatStep;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -52,6 +52,27 @@ declare class XmlReader {
|
|
|
52
52
|
failed_count: number;
|
|
53
53
|
tests: any;
|
|
54
54
|
};
|
|
55
|
+
_parseTRXTestDefinition(td: any): {
|
|
56
|
+
title: any;
|
|
57
|
+
example: any;
|
|
58
|
+
file: string;
|
|
59
|
+
description: any;
|
|
60
|
+
suite_title: any;
|
|
61
|
+
id: any;
|
|
62
|
+
};
|
|
63
|
+
_parseTRXTestResult(td: any, tests: any): {
|
|
64
|
+
suite_title: any;
|
|
65
|
+
title: any;
|
|
66
|
+
file: any;
|
|
67
|
+
description: any;
|
|
68
|
+
code: any;
|
|
69
|
+
run_time: number;
|
|
70
|
+
stack: any;
|
|
71
|
+
files: any;
|
|
72
|
+
create: boolean;
|
|
73
|
+
overwrite: boolean;
|
|
74
|
+
};
|
|
75
|
+
_mapTRXStatus(outcome: any): string;
|
|
55
76
|
processXUnit(assemblies: any): {
|
|
56
77
|
status: string;
|
|
57
78
|
create_tests: boolean;
|
package/lib/xmlReader.js
CHANGED
|
@@ -195,61 +195,17 @@ class XmlReader {
|
|
|
195
195
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
196
196
|
if (!Array.isArray(defs))
|
|
197
197
|
defs = [defs].filter(d => !!d);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (example)
|
|
202
|
-
example = { ...example[1].split(',') };
|
|
203
|
-
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
204
|
-
const suite_title = suite.pop();
|
|
205
|
-
return {
|
|
206
|
-
title,
|
|
207
|
-
example,
|
|
208
|
-
file: suite.join('/'),
|
|
209
|
-
description: td.Description,
|
|
210
|
-
suite_title,
|
|
211
|
-
id: td.Execution.id,
|
|
212
|
-
};
|
|
213
|
-
}) || [];
|
|
198
|
+
// Parse test definitions
|
|
199
|
+
const tests = defs.map(td => this._parseTRXTestDefinition(td));
|
|
200
|
+
// Parse test results
|
|
214
201
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
215
202
|
if (!Array.isArray(result))
|
|
216
203
|
result = [result].filter(d => !!d);
|
|
217
|
-
const results = result.map(td => (
|
|
218
|
-
id: td.executionId,
|
|
219
|
-
// seconds are used in junit reports, but ms are used by testomatio
|
|
220
|
-
run_time: parseFloat(td.duration) * 1000,
|
|
221
|
-
status: td.outcome,
|
|
222
|
-
stack: td.Output.StdOut,
|
|
223
|
-
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
224
|
-
}));
|
|
225
|
-
results.forEach(r => {
|
|
226
|
-
const test = tests.find(t => t.id === r.id) || {};
|
|
227
|
-
r.suite_title = test.suite_title;
|
|
228
|
-
r.title = test.title?.trim();
|
|
229
|
-
if (test.code)
|
|
230
|
-
r.code = test.code;
|
|
231
|
-
if (test.description)
|
|
232
|
-
r.description = test.description;
|
|
233
|
-
if (test.example)
|
|
234
|
-
r.example = test.example;
|
|
235
|
-
if (test.file)
|
|
236
|
-
r.file = test.file;
|
|
237
|
-
r.create = true;
|
|
238
|
-
r.overwrite = true;
|
|
239
|
-
if (r.status === 'Passed')
|
|
240
|
-
r.status = constants_js_1.STATUS.PASSED;
|
|
241
|
-
if (r.status === 'Failed')
|
|
242
|
-
r.status = constants_js_1.STATUS.FAILED;
|
|
243
|
-
if (r.status === 'Skipped')
|
|
244
|
-
r.status = constants_js_1.STATUS.SKIPPED;
|
|
245
|
-
delete r.id;
|
|
246
|
-
});
|
|
204
|
+
const results = result.map(td => this._parseTRXTestResult(td, tests));
|
|
247
205
|
debug(results);
|
|
248
206
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
249
207
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
250
|
-
|
|
251
|
-
if (failed_count > 0)
|
|
252
|
-
status = constants_js_1.STATUS.FAILED;
|
|
208
|
+
const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
|
|
253
209
|
this.tests = results.filter(t => !!t.title);
|
|
254
210
|
return {
|
|
255
211
|
status,
|
|
@@ -261,6 +217,57 @@ class XmlReader {
|
|
|
261
217
|
tests: results,
|
|
262
218
|
};
|
|
263
219
|
}
|
|
220
|
+
_parseTRXTestDefinition(td) {
|
|
221
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
222
|
+
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
223
|
+
const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
|
|
224
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
225
|
+
const suite_title = suite.pop();
|
|
226
|
+
// Convert namespace to file path for C#
|
|
227
|
+
const file = `${suite.join('/')}.cs`;
|
|
228
|
+
return {
|
|
229
|
+
title, // Base name without parameters for test import
|
|
230
|
+
example, // Parameters object for parameterized tests
|
|
231
|
+
file, // File path with .cs extension
|
|
232
|
+
description: td.Description,
|
|
233
|
+
suite_title,
|
|
234
|
+
id: td.Execution.id,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
_parseTRXTestResult(td, tests) {
|
|
238
|
+
const test = tests.find(t => t.id === td.executionId) || {};
|
|
239
|
+
const result = {
|
|
240
|
+
suite_title: test.suite_title,
|
|
241
|
+
title: test.title?.trim(),
|
|
242
|
+
file: test.file,
|
|
243
|
+
description: test.description,
|
|
244
|
+
code: test.code,
|
|
245
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
246
|
+
stack: td.Output?.StdOut || '',
|
|
247
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
248
|
+
create: true,
|
|
249
|
+
overwrite: true,
|
|
250
|
+
};
|
|
251
|
+
// Add example for parameterized tests
|
|
252
|
+
if (test.example) {
|
|
253
|
+
result.example = test.example;
|
|
254
|
+
}
|
|
255
|
+
// Map TRX status to Testomat.io status
|
|
256
|
+
result.status = this._mapTRXStatus(td.outcome);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
_mapTRXStatus(outcome) {
|
|
260
|
+
switch (outcome) {
|
|
261
|
+
case 'Passed':
|
|
262
|
+
return constants_js_1.STATUS.PASSED;
|
|
263
|
+
case 'Failed':
|
|
264
|
+
return constants_js_1.STATUS.FAILED;
|
|
265
|
+
case 'Skipped':
|
|
266
|
+
return constants_js_1.STATUS.SKIPPED;
|
|
267
|
+
default:
|
|
268
|
+
return constants_js_1.STATUS.PASSED;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
264
271
|
processXUnit(assemblies) {
|
|
265
272
|
const tests = [];
|
|
266
273
|
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -2,7 +2,7 @@ import createDebugMessages from 'debug';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import TestomatClient from '../client.js';
|
|
4
4
|
import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
5
|
-
import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
5
|
+
import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
|
|
6
6
|
import { services } from '../services/index.js';
|
|
7
7
|
import { dataStorage } from '../data-storage.js';
|
|
8
8
|
import codeceptjs from 'codeceptjs';
|
|
@@ -127,6 +127,28 @@ function CodeceptReporter(config) {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
// mark as failed all tests inside the failed hook
|
|
131
|
+
event.dispatcher.on(event.hook.failed, hook => {
|
|
132
|
+
if (hook.name !== 'BeforeSuiteHook') return;
|
|
133
|
+
const suite = hook.runnable.parent;
|
|
134
|
+
|
|
135
|
+
if (!suite) return;
|
|
136
|
+
|
|
137
|
+
const error = hook?.ctx?.currentTest?.err;
|
|
138
|
+
|
|
139
|
+
for (const test of suite.tests) {
|
|
140
|
+
client.addTestRun('failed', {
|
|
141
|
+
...stripExampleFromTitle(test.title),
|
|
142
|
+
rid: test.uid,
|
|
143
|
+
test_id: getTestomatIdFromTestTitle(test.title),
|
|
144
|
+
suite_title: stripTagsFromTitle(suite.title),
|
|
145
|
+
error,
|
|
146
|
+
time: hook?.runnable?.duration,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
130
152
|
event.dispatcher.on(event.suite.before, suite => {
|
|
131
153
|
dataStorage.setContext(suite.fullTitle());
|
|
132
154
|
});
|
|
@@ -441,7 +463,7 @@ function formatCodeceptStep(step) {
|
|
|
441
463
|
if (!step) return null;
|
|
442
464
|
|
|
443
465
|
const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
|
|
444
|
-
const title = step
|
|
466
|
+
const title = truncate(step); // Use built-in toString
|
|
445
467
|
const duration = step.duration || 0; // Use built-in duration
|
|
446
468
|
|
|
447
469
|
const formattedStep = {
|
|
@@ -469,10 +491,11 @@ function formatHookStep(step) {
|
|
|
469
491
|
if (step.actor && step.name) {
|
|
470
492
|
title = `${step.actor} ${step.name}`;
|
|
471
493
|
if (step.args && step.args.length > 0) {
|
|
472
|
-
const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
|
|
494
|
+
const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
|
|
473
495
|
title += ` ${argsStr}`;
|
|
474
496
|
}
|
|
475
497
|
}
|
|
498
|
+
title = truncate(title);
|
|
476
499
|
|
|
477
500
|
return {
|
|
478
501
|
category: 'hook',
|
|
@@ -481,5 +504,6 @@ function formatHookStep(step) {
|
|
|
481
504
|
};
|
|
482
505
|
}
|
|
483
506
|
|
|
507
|
+
|
|
484
508
|
export { CodeceptReporter };
|
|
485
509
|
export default CodeceptReporter;
|
package/src/bin/cli.js
CHANGED
|
@@ -158,7 +158,7 @@ program
|
|
|
158
158
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
159
159
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
160
160
|
.action(async (pattern, opts) => {
|
|
161
|
-
if (!pattern.endsWith('.xml')) {
|
|
161
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
162
162
|
pattern += '.xml';
|
|
163
163
|
}
|
|
164
164
|
let { javaTests, lang } = opts;
|
package/src/bin/reportXml.js
CHANGED
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
24
24
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
25
25
|
.action(async (pattern, opts) => {
|
|
26
|
-
if (!pattern.endsWith('.xml')) {
|
|
26
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
27
27
|
pattern += '.xml';
|
|
28
28
|
}
|
|
29
29
|
let { javaTests, lang } = opts;
|
package/src/bin/startTest.js
CHANGED
|
@@ -18,7 +18,7 @@ const newArgs = ['run'];
|
|
|
18
18
|
let i = 0;
|
|
19
19
|
while (i < args.length) {
|
|
20
20
|
const arg = args[i];
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (arg === '-c' || arg === '--command') {
|
|
23
23
|
// Map -c/--command to positional argument for run command
|
|
24
24
|
i++;
|
|
@@ -33,7 +33,7 @@ while (i < args.length) {
|
|
|
33
33
|
// Map --launch to start command
|
|
34
34
|
newArgs[0] = 'start';
|
|
35
35
|
} else if (arg === '--finish') {
|
|
36
|
-
// Map --finish to finish command
|
|
36
|
+
// Map --finish to finish command
|
|
37
37
|
newArgs[0] = 'finish';
|
|
38
38
|
} else {
|
|
39
39
|
// Pass through other arguments
|
|
@@ -45,9 +45,9 @@ while (i < args.length) {
|
|
|
45
45
|
// Execute the main CLI with mapped arguments
|
|
46
46
|
|
|
47
47
|
const child = spawn(process.execPath, [cliPath, ...newArgs], {
|
|
48
|
-
stdio: 'inherit'
|
|
48
|
+
stdio: 'inherit',
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
child.on('exit',
|
|
51
|
+
child.on('exit', code => {
|
|
52
52
|
process.exit(code);
|
|
53
|
-
});
|
|
53
|
+
});
|
package/src/client.js
CHANGED
|
@@ -10,7 +10,7 @@ import { glob } from 'glob';
|
|
|
10
10
|
import path, { sep } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { S3Uploader } from './uploader.js';
|
|
13
|
-
import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
|
|
13
|
+
import { formatStep, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
|
|
14
14
|
import { filesize as prettyBytes } from 'filesize';
|
|
15
15
|
|
|
16
16
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
@@ -37,9 +37,8 @@ class Client {
|
|
|
37
37
|
this.runId = '';
|
|
38
38
|
this.queue = Promise.resolve();
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const pathToPackageJSON = path.join(__dirname, '../package.json');
|
|
40
|
+
// Get package.json path - use a simple approach that works in both environments
|
|
41
|
+
const pathToPackageJSON = path.join(process.cwd(), 'package.json');
|
|
43
42
|
try {
|
|
44
43
|
this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
|
|
45
44
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
@@ -387,7 +386,11 @@ class Client {
|
|
|
387
386
|
*/
|
|
388
387
|
formatLogs({ error, steps, logs }) {
|
|
389
388
|
error = error?.trim();
|
|
390
|
-
logs = logs
|
|
389
|
+
logs = logs
|
|
390
|
+
?.trim()
|
|
391
|
+
.split('\n')
|
|
392
|
+
.map(l => truncate(l))
|
|
393
|
+
.join('\n');
|
|
391
394
|
|
|
392
395
|
if (Array.isArray(steps)) {
|
|
393
396
|
steps = steps
|
|
@@ -3,18 +3,23 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
//
|
|
7
|
-
// The xmlReader.js already extracts parameters correctly from <arguments>
|
|
6
|
+
// Extract example from title if not already present
|
|
8
7
|
if (!t.example) {
|
|
9
|
-
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
10
8
|
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
11
9
|
if (exampleMatch) {
|
|
12
|
-
//
|
|
13
|
-
|
|
10
|
+
// Extract parameters as object with numeric keys for API
|
|
11
|
+
const params = exampleMatch[1].split(',').map(param => param.trim());
|
|
12
|
+
t.example = {};
|
|
13
|
+
params.forEach((param, index) => {
|
|
14
|
+
t.example[index] = param;
|
|
15
|
+
});
|
|
14
16
|
}
|
|
15
|
-
t.title = title.trim();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// Remove parameters from title to avoid duplicates in Test Suite
|
|
20
|
+
// The example field will be used for grouping on import
|
|
21
|
+
t.title = t.title.replace(/\(.*?\)/, '').trim();
|
|
22
|
+
|
|
18
23
|
const suite = t.suite_title.split('.');
|
|
19
24
|
t.suite_title = suite.pop();
|
|
20
25
|
t.file = namespaceToFileName(t.file);
|
|
@@ -220,8 +220,20 @@ export class NUnitXmlParser {
|
|
|
220
220
|
// Build file path from suite path and class name
|
|
221
221
|
const filePath = this.buildFilePath(suitePath, className, parentSuite);
|
|
222
222
|
|
|
223
|
+
// For parameterized tests, format example as expected by Testomatio API
|
|
224
|
+
// Convert array of parameters to object with numeric keys
|
|
225
|
+
let example = null;
|
|
226
|
+
if (isParameterized && parameters.length > 0) {
|
|
227
|
+
example = {};
|
|
228
|
+
parameters.forEach((param, index) => {
|
|
229
|
+
example[index] = param;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
return {
|
|
224
|
-
|
|
234
|
+
// For runs: use full test name with parameters (TestBooleanValue(true))
|
|
235
|
+
// For import: API will group by base name using the example field
|
|
236
|
+
title: testName, // Full name with parameters for run display
|
|
225
237
|
methodName: baseMethodName || methodName || testName,
|
|
226
238
|
fullName: fullName,
|
|
227
239
|
suitePath: suitePath,
|
|
@@ -235,8 +247,9 @@ export class NUnitXmlParser {
|
|
|
235
247
|
create: true,
|
|
236
248
|
retry: false,
|
|
237
249
|
// Parameterized test metadata
|
|
250
|
+
example: example, // Parameters as object for API grouping
|
|
238
251
|
isParameterized: isParameterized,
|
|
239
|
-
parameters: parameters,
|
|
252
|
+
parameters: parameters, // Keep original array for reference
|
|
240
253
|
baseMethodName: baseMethodName,
|
|
241
254
|
};
|
|
242
255
|
}
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -459,8 +459,6 @@ class TestomatioPipe {
|
|
|
459
459
|
},
|
|
460
460
|
});
|
|
461
461
|
|
|
462
|
-
debug(APP_PREFIX, '✅ Testrun finished');
|
|
463
|
-
|
|
464
462
|
if (this.runUrl) {
|
|
465
463
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
|
|
466
464
|
}
|
|
@@ -471,7 +469,7 @@ class TestomatioPipe {
|
|
|
471
469
|
if (this.runUrl && this.proceed) {
|
|
472
470
|
const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
|
|
473
471
|
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
|
|
474
|
-
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx
|
|
472
|
+
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
|
|
475
473
|
}
|
|
476
474
|
|
|
477
475
|
if (this.hasUnmatchedTests) {
|