@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.0
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 +3 -2
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +40 -11
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +57 -152
- package/lib/data-storage.d.ts +1 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +4 -0
- 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/coverage.d.ts +82 -0
- package/lib/pipe/coverage.js +373 -0
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +44 -18
- package/lib/reporter-functions.js +14 -12
- package/lib/reporter.d.ts +31 -21
- package/lib/reporter.js +40 -5
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- 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/pipe_utils.d.ts +15 -0
- package/lib/utils/pipe_utils.js +44 -2
- package/lib/utils/utils.d.ts +6 -0
- package/lib/utils/utils.js +260 -25
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +12 -7
- 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 +51 -13
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +69 -130
- package/src/helpers.js +1 -0
- 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/coverage.js +440 -0
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/index.js +2 -0
- package/src/pipe/testomatio.js +109 -85
- package/src/reporter-functions.js +15 -12
- 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/pipe_utils.js +52 -3
- package/src/utils/utils.js +277 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
package/lib/xmlReader.js
CHANGED
|
@@ -11,6 +11,7 @@ const fast_xml_parser_1 = require("fast-xml-parser");
|
|
|
11
11
|
const constants_js_1 = require("./constants.js");
|
|
12
12
|
const crypto_1 = require("crypto");
|
|
13
13
|
const url_1 = require("url");
|
|
14
|
+
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
14
15
|
const utils_js_1 = require("./utils/utils.js");
|
|
15
16
|
const index_js_1 = require("./pipe/index.js");
|
|
16
17
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
@@ -20,7 +21,7 @@ const uploader_js_1 = require("./uploader.js");
|
|
|
20
21
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
21
22
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
22
23
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
23
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
|
|
24
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, TESTOMATIO_LEGACY_NUNIT, } = process.env;
|
|
24
25
|
const options = {
|
|
25
26
|
ignoreDeclaration: true,
|
|
26
27
|
ignoreAttributes: false,
|
|
@@ -54,6 +55,10 @@ class XmlReader {
|
|
|
54
55
|
this.stats = {};
|
|
55
56
|
this.stats.language = opts.lang?.toLowerCase();
|
|
56
57
|
this.uploader = new uploader_js_1.S3Uploader();
|
|
58
|
+
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
59
|
+
// Can be disabled via opts.enhancedNunit = false or TESTOMATIO_LEGACY_NUNIT=1
|
|
60
|
+
this.enhancedNunit = !(0, utils_js_1.transformEnvVarToBoolean)(TESTOMATIO_LEGACY_NUNIT);
|
|
61
|
+
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
57
62
|
// @ts-ignore
|
|
58
63
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
59
64
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
@@ -102,7 +107,8 @@ class XmlReader {
|
|
|
102
107
|
return this.processJUnit(jsonSuite);
|
|
103
108
|
}
|
|
104
109
|
processJUnit(jsonSuite) {
|
|
105
|
-
const { testsuite, name,
|
|
110
|
+
const { testsuite, name, failures, errors } = jsonSuite;
|
|
111
|
+
const tests = testsuite?.tests || jsonSuite.tests;
|
|
106
112
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
107
113
|
const resultTests = processTestSuite(testsuite);
|
|
108
114
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
@@ -128,6 +134,13 @@ class XmlReader {
|
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
processNUnit(jsonSuite) {
|
|
137
|
+
// Use enhanced NUnit parser if enabled and this is actually NUnit XML
|
|
138
|
+
if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
|
|
139
|
+
debug('Using enhanced NUnit parser');
|
|
140
|
+
return this.processNUnitEnhanced(jsonSuite);
|
|
141
|
+
}
|
|
142
|
+
// Fallback to legacy parser for backward compatibility
|
|
143
|
+
debug('Using legacy NUnit parser');
|
|
131
144
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
145
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
146
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
@@ -142,65 +155,58 @@ class XmlReader {
|
|
|
142
155
|
tests: resultTests,
|
|
143
156
|
};
|
|
144
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
160
|
+
* @param {Object} jsonSuite - Parsed XML suite object
|
|
161
|
+
* @returns {boolean} - True if this is NUnit XML format
|
|
162
|
+
*/
|
|
163
|
+
isNUnitXml(jsonSuite) {
|
|
164
|
+
// NUnit XML has test-suite elements with type attributes
|
|
165
|
+
if (jsonSuite['test-suite']) {
|
|
166
|
+
const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
|
|
167
|
+
// Check for NUnit-specific test-suite types
|
|
168
|
+
return (testSuite &&
|
|
169
|
+
testSuite.type &&
|
|
170
|
+
['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
processNUnitEnhanced(jsonSuite) {
|
|
175
|
+
debug('Processing NUnit XML with enhanced parser');
|
|
176
|
+
try {
|
|
177
|
+
const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
|
|
178
|
+
groupParameterized: this.groupParameterized,
|
|
179
|
+
...this.opts,
|
|
180
|
+
});
|
|
181
|
+
const result = nunitParser.parseTestRun(jsonSuite);
|
|
182
|
+
// Add parsed tests to our collection
|
|
183
|
+
this.tests = this.tests.concat(result.tests);
|
|
184
|
+
debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
|
|
189
|
+
console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
|
|
190
|
+
// Fallback to legacy parser
|
|
191
|
+
this.enhancedNunit = false;
|
|
192
|
+
return this.processNUnit(jsonSuite);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
145
195
|
processTRX(jsonSuite) {
|
|
146
196
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
147
197
|
if (!Array.isArray(defs))
|
|
148
198
|
defs = [defs].filter(d => !!d);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (example)
|
|
153
|
-
example = { ...example[1].split(',') };
|
|
154
|
-
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
155
|
-
const suite_title = suite.pop();
|
|
156
|
-
return {
|
|
157
|
-
title,
|
|
158
|
-
example,
|
|
159
|
-
file: suite.join('/'),
|
|
160
|
-
description: td.Description,
|
|
161
|
-
suite_title,
|
|
162
|
-
id: td.Execution.id,
|
|
163
|
-
};
|
|
164
|
-
}) || [];
|
|
199
|
+
// Parse test definitions
|
|
200
|
+
const tests = defs.map(td => this._parseTRXTestDefinition(td));
|
|
201
|
+
// Parse test results
|
|
165
202
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
166
203
|
if (!Array.isArray(result))
|
|
167
204
|
result = [result].filter(d => !!d);
|
|
168
|
-
const results = result.map(td => (
|
|
169
|
-
id: td.executionId,
|
|
170
|
-
// seconds are used in junit reports, but ms are used by testomatio
|
|
171
|
-
run_time: parseFloat(td.duration) * 1000,
|
|
172
|
-
status: td.outcome,
|
|
173
|
-
stack: td.Output.StdOut,
|
|
174
|
-
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
175
|
-
}));
|
|
176
|
-
results.forEach(r => {
|
|
177
|
-
const test = tests.find(t => t.id === r.id) || {};
|
|
178
|
-
r.suite_title = test.suite_title;
|
|
179
|
-
r.title = test.title?.trim();
|
|
180
|
-
if (test.code)
|
|
181
|
-
r.code = test.code;
|
|
182
|
-
if (test.description)
|
|
183
|
-
r.description = test.description;
|
|
184
|
-
if (test.example)
|
|
185
|
-
r.example = test.example;
|
|
186
|
-
if (test.file)
|
|
187
|
-
r.file = test.file;
|
|
188
|
-
r.create = true;
|
|
189
|
-
r.overwrite = true;
|
|
190
|
-
if (r.status === 'Passed')
|
|
191
|
-
r.status = constants_js_1.STATUS.PASSED;
|
|
192
|
-
if (r.status === 'Failed')
|
|
193
|
-
r.status = constants_js_1.STATUS.FAILED;
|
|
194
|
-
if (r.status === 'Skipped')
|
|
195
|
-
r.status = constants_js_1.STATUS.SKIPPED;
|
|
196
|
-
delete r.id;
|
|
197
|
-
});
|
|
205
|
+
const results = result.map(td => this._parseTRXTestResult(td, tests));
|
|
198
206
|
debug(results);
|
|
199
207
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
200
208
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
201
|
-
|
|
202
|
-
if (failed_count > 0)
|
|
203
|
-
status = constants_js_1.STATUS.FAILED;
|
|
209
|
+
const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
|
|
204
210
|
this.tests = results.filter(t => !!t.title);
|
|
205
211
|
return {
|
|
206
212
|
status,
|
|
@@ -212,6 +218,64 @@ class XmlReader {
|
|
|
212
218
|
tests: results,
|
|
213
219
|
};
|
|
214
220
|
}
|
|
221
|
+
_parseTRXTestDefinition(td) {
|
|
222
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
223
|
+
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
224
|
+
const example = exampleMatch
|
|
225
|
+
? {
|
|
226
|
+
...exampleMatch[1]
|
|
227
|
+
.split(',')
|
|
228
|
+
.map(p => p.trim())
|
|
229
|
+
.filter(p => p !== ''),
|
|
230
|
+
}
|
|
231
|
+
: null;
|
|
232
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
233
|
+
const suite_title = suite.pop();
|
|
234
|
+
// Convert namespace to file path for C#
|
|
235
|
+
const file = `${suite.join('/')}.cs`;
|
|
236
|
+
return {
|
|
237
|
+
title, // Base name without parameters for test import
|
|
238
|
+
example, // Parameters object for parameterized tests
|
|
239
|
+
file, // File path with .cs extension
|
|
240
|
+
description: td.Description,
|
|
241
|
+
suite_title,
|
|
242
|
+
id: td.Execution.id,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
_parseTRXTestResult(td, tests) {
|
|
246
|
+
const test = tests.find(t => t.id === td.executionId) || {};
|
|
247
|
+
const result = {
|
|
248
|
+
suite_title: test.suite_title,
|
|
249
|
+
title: test.title?.trim(),
|
|
250
|
+
file: test.file,
|
|
251
|
+
description: test.description,
|
|
252
|
+
code: test.code,
|
|
253
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
254
|
+
stack: td.Output?.StdOut || '',
|
|
255
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
256
|
+
create: true,
|
|
257
|
+
overwrite: true,
|
|
258
|
+
};
|
|
259
|
+
// Add example for parameterized tests
|
|
260
|
+
if (test.example) {
|
|
261
|
+
result.example = test.example;
|
|
262
|
+
}
|
|
263
|
+
// Map TRX status to Testomat.io status
|
|
264
|
+
result.status = this._mapTRXStatus(td.outcome);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
_mapTRXStatus(outcome) {
|
|
268
|
+
switch (outcome) {
|
|
269
|
+
case 'Passed':
|
|
270
|
+
return constants_js_1.STATUS.PASSED;
|
|
271
|
+
case 'Failed':
|
|
272
|
+
return constants_js_1.STATUS.FAILED;
|
|
273
|
+
case 'Skipped':
|
|
274
|
+
return constants_js_1.STATUS.SKIPPED;
|
|
275
|
+
default:
|
|
276
|
+
return constants_js_1.STATUS.PASSED;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
215
279
|
processXUnit(assemblies) {
|
|
216
280
|
const tests = [];
|
|
217
281
|
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
|
@@ -459,7 +523,12 @@ function reduceTestCases(prev, item) {
|
|
|
459
523
|
tags ||= [];
|
|
460
524
|
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
461
525
|
if (exampleMatches) {
|
|
462
|
-
example = {
|
|
526
|
+
example = {
|
|
527
|
+
...exampleMatches[1]
|
|
528
|
+
.split(',')
|
|
529
|
+
.map(v => v.trim().replace(/[^\w\s-]/g, ''))
|
|
530
|
+
.filter(v => v !== ''),
|
|
531
|
+
};
|
|
463
532
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
464
533
|
}
|
|
465
534
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
8
|
+
"main": "lib/reporter.js",
|
|
9
|
+
"module": "src/reporter.js",
|
|
10
|
+
"types": "types/types.d.ts",
|
|
9
11
|
"repository": "git@github.com:testomatio/reporter.git",
|
|
10
12
|
"author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
|
|
11
13
|
"license": "MIT",
|
|
@@ -16,21 +18,22 @@
|
|
|
16
18
|
"@cucumber/cucumber": "^10.9.0",
|
|
17
19
|
"@octokit/rest": "^21.1.1",
|
|
18
20
|
"aws-sdk": "^2.1072.0",
|
|
19
|
-
"gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
|
|
20
21
|
"callsite-record": "^4.1.4",
|
|
21
22
|
"commander": "^12",
|
|
22
23
|
"cross-spawn": "^7.0.3",
|
|
23
24
|
"csv-writer": "^1.6.0",
|
|
24
|
-
"debug": "
|
|
25
|
+
"debug": "4.3.4",
|
|
25
26
|
"dotenv": "^16.0.1",
|
|
26
27
|
"fast-xml-parser": "^4.4.1",
|
|
27
28
|
"file-url": "3.0.0",
|
|
28
29
|
"filesize": "^10.1.6",
|
|
30
|
+
"gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
|
|
29
31
|
"glob": "^10.3",
|
|
30
32
|
"handlebars": "^4.7.8",
|
|
31
33
|
"has-flag": "^5.0.1",
|
|
32
34
|
"humanize-duration": "^3.27.3",
|
|
33
35
|
"is-valid-path": "^0.1.1",
|
|
36
|
+
"js-yaml": "^4.1.1",
|
|
34
37
|
"json-cycle": "^1.3.0",
|
|
35
38
|
"lodash.memoize": "^4.1.2",
|
|
36
39
|
"lodash.merge": "^4.6.2",
|
|
@@ -38,14 +41,15 @@
|
|
|
38
41
|
"picocolors": "^1.0.1",
|
|
39
42
|
"pretty-ms": "^7.0.1",
|
|
40
43
|
"promise-retry": "^2.0.1",
|
|
41
|
-
"strip-ansi": "
|
|
44
|
+
"strip-ansi": "7.1.0",
|
|
42
45
|
"uuid": "^9.0.0"
|
|
43
46
|
},
|
|
44
47
|
"files": [
|
|
45
48
|
"bin",
|
|
46
49
|
"lib",
|
|
47
50
|
"src",
|
|
48
|
-
"testcafe"
|
|
51
|
+
"testcafe",
|
|
52
|
+
"types"
|
|
49
53
|
],
|
|
50
54
|
"scripts": {
|
|
51
55
|
"clear-exportdir": "rm -rf export/",
|
|
@@ -57,7 +61,8 @@
|
|
|
57
61
|
"test": "mocha 'tests/unit/**/*_test.js'",
|
|
58
62
|
"test:playwright": "mocha tests/adapter/playwright.test.js",
|
|
59
63
|
"test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
|
|
60
|
-
"test:
|
|
64
|
+
"test:vitest": "mocha tests/adapter/vitest.test.js",
|
|
65
|
+
"test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest",
|
|
61
66
|
"test:all": "npm run test && npm run test:frameworks",
|
|
62
67
|
"test:adapters": "mocha tests/adapter/*.test.js",
|
|
63
68
|
"test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
|
package/src/adapter/codecept.js
CHANGED
|
@@ -17,12 +17,15 @@ if (!global.codeceptjs) {
|
|
|
17
17
|
// @ts-ignore
|
|
18
18
|
const { event, recorder, codecept, output } = global.codeceptjs;
|
|
19
19
|
|
|
20
|
-
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
20
|
+
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
21
|
+
.version()
|
|
22
|
+
.match(/(\d+)\.(\d+)/)
|
|
23
|
+
.map(Number);
|
|
21
24
|
|
|
22
25
|
// Constants for hook execution order
|
|
23
26
|
const HOOK_EXECUTION_ORDER = {
|
|
24
27
|
PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
|
|
25
|
-
POST_TEST: ['AfterHook', 'AfterSuiteHook']
|
|
28
|
+
POST_TEST: ['AfterHook', 'AfterSuiteHook'],
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
// codeceptjs workers are self-contained
|
|
@@ -35,7 +38,9 @@ if (MAJOR_VERSION < 3) {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
|
|
38
|
-
console.log(
|
|
41
|
+
console.log(
|
|
42
|
+
'🔴 CodeceptJS 3.7+ is supported, please upgrade CodeceptJS or use 1.6 version of `@testomatio/reporter`',
|
|
43
|
+
);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function CodeceptReporter(config) {
|
|
@@ -57,18 +62,18 @@ function CodeceptReporter(config) {
|
|
|
57
62
|
say: output.say,
|
|
58
63
|
};
|
|
59
64
|
|
|
60
|
-
output.debug = function(msg) {
|
|
65
|
+
output.debug = function (msg) {
|
|
61
66
|
originalOutput.debug(msg);
|
|
62
67
|
dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.cyan(msg.toString()));
|
|
63
68
|
};
|
|
64
69
|
|
|
65
|
-
output.say = function(message, color = 'cyan') {
|
|
70
|
+
output.say = function (message, color = 'cyan') {
|
|
66
71
|
originalOutput.say(message, color);
|
|
67
72
|
const sayMsg = repeat(this?.stepShift || 0) + ` ${pc.bold(pc[color](message))}`;
|
|
68
73
|
dataStorage.putData('log', sayMsg);
|
|
69
74
|
};
|
|
70
75
|
|
|
71
|
-
output.log = function(msg) {
|
|
76
|
+
output.log = function (msg) {
|
|
72
77
|
originalOutput.log(msg);
|
|
73
78
|
dataStorage.putData('log', repeat(this?.stepShift || 0) + pc.gray(msg));
|
|
74
79
|
};
|
|
@@ -108,7 +113,7 @@ function CodeceptReporter(config) {
|
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
// Hook event listeners
|
|
111
|
-
event.dispatcher.on(event.hook.started,
|
|
116
|
+
event.dispatcher.on(event.hook.started, hook => {
|
|
112
117
|
output.stepShift = 2;
|
|
113
118
|
currentHook = hook.name;
|
|
114
119
|
let title = hook.hookName;
|
|
@@ -126,7 +131,6 @@ function CodeceptReporter(config) {
|
|
|
126
131
|
services.setContext(null);
|
|
127
132
|
});
|
|
128
133
|
|
|
129
|
-
|
|
130
134
|
// mark as failed all tests inside the failed hook
|
|
131
135
|
event.dispatcher.on(event.hook.failed, hook => {
|
|
132
136
|
if (hook.name !== 'BeforeSuiteHook') return;
|
|
@@ -148,7 +152,6 @@ function CodeceptReporter(config) {
|
|
|
148
152
|
}
|
|
149
153
|
});
|
|
150
154
|
|
|
151
|
-
|
|
152
155
|
event.dispatcher.on(event.suite.before, suite => {
|
|
153
156
|
dataStorage.setContext(suite.fullTitle());
|
|
154
157
|
});
|
|
@@ -167,7 +170,7 @@ function CodeceptReporter(config) {
|
|
|
167
170
|
testTimeMap[test.uid] = Date.now();
|
|
168
171
|
});
|
|
169
172
|
|
|
170
|
-
event.dispatcher.on(event.all.result, async
|
|
173
|
+
event.dispatcher.on(event.all.result, async result => {
|
|
171
174
|
debug('waiting for all tests to be reported');
|
|
172
175
|
// all tests were reported and we can upload videos
|
|
173
176
|
await Promise.all(reportTestPromises);
|
|
@@ -327,7 +330,7 @@ function captureHookStep(step, currentHook, hookSteps) {
|
|
|
327
330
|
status: step.status,
|
|
328
331
|
startTime,
|
|
329
332
|
endTime,
|
|
330
|
-
helperMethod: step.helperMethod
|
|
333
|
+
helperMethod: step.helperMethod,
|
|
331
334
|
});
|
|
332
335
|
hookSteps.set(currentHook, hookStepsArray);
|
|
333
336
|
}
|
|
@@ -422,13 +425,12 @@ function processTestSteps(steps, hierarchy) {
|
|
|
422
425
|
}
|
|
423
426
|
}
|
|
424
427
|
|
|
425
|
-
|
|
426
428
|
function createSectionStep(metaStep) {
|
|
427
429
|
return {
|
|
428
430
|
category: 'user',
|
|
429
431
|
title: metaStep.toString(), // Use built-in toString method
|
|
430
432
|
duration: metaStep.duration || 0, // Use built-in duration
|
|
431
|
-
steps: []
|
|
433
|
+
steps: [],
|
|
432
434
|
};
|
|
433
435
|
}
|
|
434
436
|
|
|
@@ -439,7 +441,7 @@ function createHookSection(hookName, steps) {
|
|
|
439
441
|
category: 'hook',
|
|
440
442
|
title: formatHookName(hookName),
|
|
441
443
|
duration: 0,
|
|
442
|
-
steps: []
|
|
444
|
+
steps: [],
|
|
443
445
|
};
|
|
444
446
|
|
|
445
447
|
for (const step of steps) {
|
|
@@ -457,7 +459,6 @@ function formatHookName(hookName) {
|
|
|
457
459
|
return hookName.replace(/Hook$/, '');
|
|
458
460
|
}
|
|
459
461
|
|
|
460
|
-
|
|
461
462
|
// Format CodeceptJS step using its built-in methods
|
|
462
463
|
function formatCodeceptStep(step) {
|
|
463
464
|
if (!step) return null;
|
|
@@ -469,14 +470,14 @@ function formatCodeceptStep(step) {
|
|
|
469
470
|
const formattedStep = {
|
|
470
471
|
category,
|
|
471
472
|
title,
|
|
472
|
-
duration
|
|
473
|
+
duration,
|
|
473
474
|
};
|
|
474
475
|
|
|
475
476
|
// Add error if step failed
|
|
476
477
|
if (step.status === 'failed' && step.err) {
|
|
477
478
|
formattedStep.error = {
|
|
478
479
|
message: step.err.message || 'Step failed',
|
|
479
|
-
stack: step.err.stack || ''
|
|
480
|
+
stack: step.err.stack || '',
|
|
480
481
|
};
|
|
481
482
|
}
|
|
482
483
|
|
|
@@ -500,10 +501,9 @@ function formatHookStep(step) {
|
|
|
500
501
|
return {
|
|
501
502
|
category: 'hook',
|
|
502
503
|
title,
|
|
503
|
-
duration: step.duration || 0
|
|
504
|
+
duration: step.duration || 0,
|
|
504
505
|
};
|
|
505
506
|
}
|
|
506
507
|
|
|
507
|
-
|
|
508
508
|
export { CodeceptReporter };
|
|
509
509
|
export default CodeceptReporter;
|
package/src/adapter/mocha.js
CHANGED
|
@@ -84,7 +84,7 @@ function MochaReporter(runner, opts) {
|
|
|
84
84
|
const artifacts = services.artifacts.get(test.fullTitle());
|
|
85
85
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
86
86
|
const links = services.links.get(test.fullTitle());
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
client.addTestRun(STATUS.SKIPPED, {
|
|
89
89
|
title: getTestName(test),
|
|
90
90
|
suite_title: getSuiteTitle(test),
|
|
@@ -254,7 +254,7 @@ function generateTmpFilepath(filename = '') {
|
|
|
254
254
|
*/
|
|
255
255
|
function extractTags(test) {
|
|
256
256
|
const tagsSet = new Set();
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
// Extract tags from test title (@tag format)
|
|
259
259
|
const titleTagsMatch = test.title.match(/@\w+/g);
|
|
260
260
|
if (titleTagsMatch) {
|
|
@@ -262,7 +262,7 @@ function extractTags(test) {
|
|
|
262
262
|
tagsSet.add(tag.replace('@', '').toLowerCase());
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
// Extract tags from test.tags (Playwright built-in tags)
|
|
267
267
|
if (test.tags && Array.isArray(test.tags)) {
|
|
268
268
|
test.tags.forEach(tag => {
|
package/src/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import createDebugMessages from 'debug';
|
|
|
7
7
|
import TestomatClient from '../client.js';
|
|
8
8
|
import XmlReader from '../xmlReader.js';
|
|
9
9
|
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
10
|
-
import { cleanLatestRunId, getPackageVersion } from '../utils/utils.js';
|
|
10
|
+
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
11
11
|
import { config } from '../config.js';
|
|
12
12
|
import { readLatestRunId } from '../utils/utils.js';
|
|
13
13
|
import pc from 'picocolors';
|
|
@@ -35,14 +35,20 @@ program
|
|
|
35
35
|
program
|
|
36
36
|
.command('start')
|
|
37
37
|
.description('Start a new run and return its ID')
|
|
38
|
-
.
|
|
38
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
39
|
+
.action(async opts => {
|
|
39
40
|
cleanLatestRunId();
|
|
40
41
|
|
|
41
42
|
console.log('Starting a new Run on Testomat.io...');
|
|
42
43
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
43
44
|
const client = new TestomatClient({ apiKey });
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
const createRunParams = {};
|
|
47
|
+
if (opts.kind) {
|
|
48
|
+
createRunParams.kind = opts.kind;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
client.createRun(createRunParams).then(() => {
|
|
46
52
|
console.log(process.env.runId);
|
|
47
53
|
process.exit(0);
|
|
48
54
|
});
|
|
@@ -75,6 +81,8 @@ program
|
|
|
75
81
|
.description('Run tests with the specified command')
|
|
76
82
|
.argument('<command>', 'Test runner command')
|
|
77
83
|
.option('--filter <filter>', 'Additional execution filter')
|
|
84
|
+
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
85
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
78
86
|
.action(async (command, opts) => {
|
|
79
87
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
80
88
|
const title = process.env.TESTOMATIO_TITLE;
|
|
@@ -86,17 +94,39 @@ program
|
|
|
86
94
|
|
|
87
95
|
const client = new TestomatClient({ apiKey, title });
|
|
88
96
|
|
|
89
|
-
if (opts.filter) {
|
|
90
|
-
|
|
97
|
+
if (opts.filter || opts.filterList) {
|
|
98
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
|
|
99
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
|
|
100
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
|
|
101
|
+
const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
|
|
91
102
|
const pipeOptions = optsArray.join(':');
|
|
92
103
|
|
|
104
|
+
const prepareRunParams = { pipe, pipeOptions };
|
|
105
|
+
|
|
93
106
|
try {
|
|
94
|
-
const tests = await client.prepareRun(
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
const tests = await client.prepareRun(prepareRunParams);
|
|
108
|
+
|
|
109
|
+
if (!tests || tests.length === 0) {
|
|
110
|
+
console.log(APP_PREFIX, pc.yellow('No tests found.'));
|
|
111
|
+
return;
|
|
97
112
|
}
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
|
|
114
|
+
const pattern = `(${tests.join('|')})`;
|
|
115
|
+
const filteredCommand = applyFilter(command, tests);
|
|
116
|
+
|
|
117
|
+
debug(`Execution pattern: "${pattern}"`);
|
|
118
|
+
|
|
119
|
+
if(opts.filterList) {
|
|
120
|
+
console.log(APP_PREFIX, pc.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
|
|
121
|
+
console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
command = filteredCommand;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.log(APP_PREFIX, err.message || err);
|
|
129
|
+
return;
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
@@ -106,7 +136,7 @@ program
|
|
|
106
136
|
const testCmds = command.split(' ');
|
|
107
137
|
const cmd = spawn(testCmds[0], testCmds.slice(1), {
|
|
108
138
|
stdio: 'inherit',
|
|
109
|
-
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
|
|
139
|
+
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
|
|
110
140
|
});
|
|
111
141
|
|
|
112
142
|
cmd.on('close', async code => {
|
|
@@ -120,8 +150,16 @@ program
|
|
|
120
150
|
});
|
|
121
151
|
};
|
|
122
152
|
|
|
153
|
+
const createRunParams = {};
|
|
154
|
+
if (title) {
|
|
155
|
+
createRunParams.title = title;
|
|
156
|
+
}
|
|
157
|
+
if (opts.kind) {
|
|
158
|
+
createRunParams.kind = opts.kind;
|
|
159
|
+
}
|
|
160
|
+
|
|
123
161
|
if (apiKey) {
|
|
124
|
-
await client.createRun().then(runTests);
|
|
162
|
+
await client.createRun(createRunParams).then(runTests);
|
|
125
163
|
} else {
|
|
126
164
|
await runTests();
|
|
127
165
|
}
|
|
@@ -158,7 +196,7 @@ program
|
|
|
158
196
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
159
197
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
160
198
|
.action(async (pattern, opts) => {
|
|
161
|
-
if (!pattern.endsWith('.xml')) {
|
|
199
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
162
200
|
pattern += '.xml';
|
|
163
201
|
}
|
|
164
202
|
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;
|
|
@@ -34,7 +34,10 @@ program
|
|
|
34
34
|
}
|
|
35
35
|
lang = lang?.toLowerCase();
|
|
36
36
|
if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
|
|
37
|
-
const runReader = new XmlReader({
|
|
37
|
+
const runReader = new XmlReader({
|
|
38
|
+
javaTests,
|
|
39
|
+
lang,
|
|
40
|
+
});
|
|
38
41
|
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
39
42
|
if (!files.length) {
|
|
40
43
|
console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
|