@testomatio/reporter 2.1.3-beta.1-xml-import → 2.1.3-beta.1-multi-links
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 +5 -6
- package/lib/adapter/mocha.js +0 -14
- package/lib/adapter/webdriver.js +4 -6
- package/lib/bin/startTest.js +91 -38
- package/lib/client.js +3 -6
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/pipe/testomatio.js +2 -1
- package/lib/reporter-functions.d.ts +7 -20
- package/lib/reporter-functions.js +35 -27
- package/lib/reporter.d.ts +20 -22
- package/lib/reporter.js +7 -9
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -2
- package/lib/services/index.js +2 -2
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +1 -1
- package/lib/services/labels.js +2 -2
- package/lib/services/logger.d.ts +1 -1
- package/lib/utils/cli_utils.d.ts +1 -0
- package/lib/utils/cli_utils.js +65552 -0
- package/lib/utils/utils.js +1 -3
- package/lib/xmlReader.d.ts +0 -7
- package/lib/xmlReader.js +7 -180
- package/package.json +1 -1
- package/src/adapter/codecept.js +5 -6
- package/src/adapter/mocha.js +0 -15
- package/src/adapter/webdriver.js +4 -6
- package/src/bin/startTest.js +114 -43
- package/src/client.js +3 -5
- package/src/data-storage.js +6 -6
- package/src/pipe/testomatio.js +2 -1
- package/src/reporter-functions.js +37 -27
- package/src/reporter.js +6 -8
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/utils/utils.js +3 -5
- package/src/xmlReader.js +7 -210
- package/src/services/links.js +0 -69
package/lib/utils/utils.js
CHANGED
|
@@ -321,7 +321,7 @@ const fileSystem = {
|
|
|
321
321
|
exports.fileSystem = fileSystem;
|
|
322
322
|
const foundedTestLog = (app, tests) => {
|
|
323
323
|
const n = tests.length;
|
|
324
|
-
return console.log(app, `✅ We found
|
|
324
|
+
return n === 1 ? console.log(app, `✅ We found one test!`) : console.log(app, `✅ We found ${n} tests!`);
|
|
325
325
|
};
|
|
326
326
|
exports.foundedTestLog = foundedTestLog;
|
|
327
327
|
const humanize = text => {
|
|
@@ -399,8 +399,6 @@ function storeRunId(runId) {
|
|
|
399
399
|
function readLatestRunId() {
|
|
400
400
|
try {
|
|
401
401
|
const filePath = path_1.default.join(os_1.default.tmpdir(), `testomatio.latest.run`);
|
|
402
|
-
if (!fs_1.default.existsSync(filePath))
|
|
403
|
-
return null;
|
|
404
402
|
const stats = fs_1.default.statSync(filePath);
|
|
405
403
|
const diff = +new Date() - +stats.mtime;
|
|
406
404
|
const diffHours = diff / 1000 / 60 / 60;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -77,13 +77,6 @@ declare class XmlReader {
|
|
|
77
77
|
skipped_count: number;
|
|
78
78
|
tests: any[];
|
|
79
79
|
};
|
|
80
|
-
deduplicateTestsByFQN(tests: any): any[];
|
|
81
|
-
generateFQN(test: any): string;
|
|
82
|
-
generateNormalizedFQN(test: any): string;
|
|
83
|
-
extractAssemblyName(test: any): any;
|
|
84
|
-
extractNamespace(test: any): any;
|
|
85
|
-
extractClassName(test: any): any;
|
|
86
|
-
extractCsFileFromPath(test: any): any;
|
|
87
80
|
calculateStats(): {};
|
|
88
81
|
fetchSourceCode(): void;
|
|
89
82
|
formatTests(): void;
|
package/lib/xmlReader.js
CHANGED
|
@@ -131,9 +131,7 @@ class XmlReader {
|
|
|
131
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
134
|
-
|
|
135
|
-
const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
|
|
136
|
-
this.tests = this.tests.concat(deduplicatedTests);
|
|
134
|
+
this.tests = this.tests.concat(resultTests);
|
|
137
135
|
return {
|
|
138
136
|
status: result?.toLowerCase(),
|
|
139
137
|
create_tests: true,
|
|
@@ -141,7 +139,7 @@ class XmlReader {
|
|
|
141
139
|
passed_count: parseInt(passed, 10),
|
|
142
140
|
failed_count: parseInt(failed, 10),
|
|
143
141
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
144
|
-
tests:
|
|
142
|
+
tests: resultTests,
|
|
145
143
|
};
|
|
146
144
|
}
|
|
147
145
|
processTRX(jsonSuite) {
|
|
@@ -277,125 +275,6 @@ class XmlReader {
|
|
|
277
275
|
tests,
|
|
278
276
|
};
|
|
279
277
|
}
|
|
280
|
-
deduplicateTestsByFQN(tests) {
|
|
281
|
-
const fqnMap = new Map();
|
|
282
|
-
tests.forEach(test => {
|
|
283
|
-
const fqn = this.generateNormalizedFQN(test);
|
|
284
|
-
if (fqnMap.has(fqn)) {
|
|
285
|
-
const existingTest = fqnMap.get(fqn);
|
|
286
|
-
// Merge test properties, prioritizing Test Explorer structure but updating with IDs
|
|
287
|
-
if (test.test_id && !existingTest.test_id) {
|
|
288
|
-
existingTest.test_id = test.test_id;
|
|
289
|
-
}
|
|
290
|
-
// Keep the most complete test data
|
|
291
|
-
if (test.stack && !existingTest.stack) {
|
|
292
|
-
existingTest.stack = test.stack;
|
|
293
|
-
}
|
|
294
|
-
if (test.message && !existingTest.message) {
|
|
295
|
-
existingTest.message = test.message;
|
|
296
|
-
}
|
|
297
|
-
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
298
|
-
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
299
|
-
existingTest.suite_title = test.suite_title;
|
|
300
|
-
existingTest.file = this.extractCsFileFromPath(test);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
// Fix file path to use proper .cs file names from source paths
|
|
305
|
-
test.file = this.extractCsFileFromPath(test);
|
|
306
|
-
fqnMap.set(fqn, test);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
return Array.from(fqnMap.values());
|
|
310
|
-
}
|
|
311
|
-
generateFQN(test) {
|
|
312
|
-
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
313
|
-
// Don't include assembly as it can vary between different test structures
|
|
314
|
-
const namespace = this.extractNamespace(test);
|
|
315
|
-
const className = this.extractClassName(test);
|
|
316
|
-
const methodName = test.title;
|
|
317
|
-
// Use the most complete namespace.class structure available
|
|
318
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
319
|
-
return `${test.suite_title}.${methodName}`;
|
|
320
|
-
}
|
|
321
|
-
return `${namespace}.${className}.${methodName}`;
|
|
322
|
-
}
|
|
323
|
-
generateNormalizedFQN(test) {
|
|
324
|
-
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
325
|
-
// This normalizes different representations of the same test
|
|
326
|
-
const fullClassName = test.suite_title || '';
|
|
327
|
-
const methodName = test.title;
|
|
328
|
-
// Extract the most specific namespace.class pattern
|
|
329
|
-
if (fullClassName.includes('.')) {
|
|
330
|
-
const parts = fullClassName.split('.');
|
|
331
|
-
if (parts.length >= 2) {
|
|
332
|
-
const className = parts[parts.length - 1];
|
|
333
|
-
// Look for common .NET namespace patterns and normalize them:
|
|
334
|
-
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
335
|
-
// Tests.MyClass -> Tests.MyClass
|
|
336
|
-
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
337
|
-
let normalizedNamespace = '';
|
|
338
|
-
for (let i = parts.length - 2; i >= 0; i--) {
|
|
339
|
-
const part = parts[i];
|
|
340
|
-
// Build namespace from right to left, excluding project names
|
|
341
|
-
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
342
|
-
// Found a test namespace, use it as the normalized namespace
|
|
343
|
-
normalizedNamespace = part;
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
else if (i === parts.length - 2) {
|
|
347
|
-
// If no test namespace found, use the immediate parent as namespace
|
|
348
|
-
normalizedNamespace = part;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// Fallback for simple class names
|
|
355
|
-
return `${fullClassName}.${methodName}`;
|
|
356
|
-
}
|
|
357
|
-
extractAssemblyName(test) {
|
|
358
|
-
// Extract assembly name from file path or use default
|
|
359
|
-
if (test.file) {
|
|
360
|
-
const parts = test.file.split(/[/\\]/);
|
|
361
|
-
return parts[0] || 'DefaultAssembly';
|
|
362
|
-
}
|
|
363
|
-
return 'DefaultAssembly';
|
|
364
|
-
}
|
|
365
|
-
extractNamespace(test) {
|
|
366
|
-
// Extract namespace from suite_title or classname
|
|
367
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
368
|
-
const parts = test.suite_title.split('.');
|
|
369
|
-
return parts.slice(0, -1).join('.');
|
|
370
|
-
}
|
|
371
|
-
return test.suite_title || 'DefaultNamespace';
|
|
372
|
-
}
|
|
373
|
-
extractClassName(test) {
|
|
374
|
-
// Extract class name from suite_title
|
|
375
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
376
|
-
const parts = test.suite_title.split('.');
|
|
377
|
-
return parts[parts.length - 1];
|
|
378
|
-
}
|
|
379
|
-
return test.suite_title || 'DefaultClass';
|
|
380
|
-
}
|
|
381
|
-
extractCsFileFromPath(test) {
|
|
382
|
-
// Extract .cs file name from source file path, not namespace
|
|
383
|
-
if (test.file) {
|
|
384
|
-
// Look for actual .cs file path patterns
|
|
385
|
-
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
386
|
-
if (csFileMatch) {
|
|
387
|
-
return test.file;
|
|
388
|
-
}
|
|
389
|
-
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
390
|
-
const className = this.extractClassName(test);
|
|
391
|
-
const pathParts = test.file.split(/[/\\]/);
|
|
392
|
-
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
393
|
-
return pathParts.join('/');
|
|
394
|
-
}
|
|
395
|
-
// Fallback to class name
|
|
396
|
-
const className = this.extractClassName(test);
|
|
397
|
-
return `${className}.cs`;
|
|
398
|
-
}
|
|
399
278
|
calculateStats() {
|
|
400
279
|
this.stats = {
|
|
401
280
|
...this.stats,
|
|
@@ -551,8 +430,7 @@ function reduceTestCases(prev, item) {
|
|
|
551
430
|
testCases
|
|
552
431
|
.filter(t => !!t)
|
|
553
432
|
.forEach(testCaseItem => {
|
|
554
|
-
|
|
555
|
-
const file = extractSourceFilePath(testCaseItem, item);
|
|
433
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
556
434
|
let stack = '';
|
|
557
435
|
let message = '';
|
|
558
436
|
if (testCaseItem.error)
|
|
@@ -572,11 +450,11 @@ function reduceTestCases(prev, item) {
|
|
|
572
450
|
if (!message)
|
|
573
451
|
message = stack.trim().split('\n')[0];
|
|
574
452
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
453
|
+
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
575
454
|
// SpecFlow config
|
|
576
455
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
577
456
|
let example = null;
|
|
578
|
-
|
|
579
|
-
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
457
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
580
458
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
581
459
|
tags ||= [];
|
|
582
460
|
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
@@ -638,51 +516,6 @@ function reduceTestCases(prev, item) {
|
|
|
638
516
|
});
|
|
639
517
|
return prev;
|
|
640
518
|
}
|
|
641
|
-
function extractSourceFilePath(testCaseItem, item) {
|
|
642
|
-
// Priority order for file path extraction to match Test Explorer structure:
|
|
643
|
-
// 1. fullname (contains full project path)
|
|
644
|
-
// 2. filepath (direct file path)
|
|
645
|
-
// 3. file attribute from test case
|
|
646
|
-
// 4. package (fallback)
|
|
647
|
-
if (item.fullname) {
|
|
648
|
-
// Extract actual file path from fullname if it contains path separators
|
|
649
|
-
const fullnameParts = item.fullname.split('.');
|
|
650
|
-
if (fullnameParts.length > 2) {
|
|
651
|
-
// Reconstruct path from project.namespace.class structure
|
|
652
|
-
const projectName = fullnameParts[0];
|
|
653
|
-
const namespaceParts = fullnameParts.slice(1, -1);
|
|
654
|
-
const className = fullnameParts[fullnameParts.length - 1];
|
|
655
|
-
return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (item.filepath)
|
|
659
|
-
return item.filepath;
|
|
660
|
-
if (testCaseItem.file)
|
|
661
|
-
return testCaseItem.file;
|
|
662
|
-
if (item.package)
|
|
663
|
-
return item.package;
|
|
664
|
-
// Fallback: construct from classname
|
|
665
|
-
if (testCaseItem.classname) {
|
|
666
|
-
const parts = testCaseItem.classname.split('.');
|
|
667
|
-
const className = parts[parts.length - 1];
|
|
668
|
-
const namespacePath = parts.slice(0, -1).join('/');
|
|
669
|
-
return `${namespacePath}/${className}.cs`;
|
|
670
|
-
}
|
|
671
|
-
return '';
|
|
672
|
-
}
|
|
673
|
-
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
674
|
-
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
675
|
-
// Priority: fullname > classname > name
|
|
676
|
-
if (item.fullname) {
|
|
677
|
-
// Use fullname to maintain Test Explorer structure
|
|
678
|
-
return item.fullname;
|
|
679
|
-
}
|
|
680
|
-
if (testCaseItem.classname) {
|
|
681
|
-
return testCaseItem.classname;
|
|
682
|
-
}
|
|
683
|
-
// Fallback to item name but prefer classname structure
|
|
684
|
-
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
685
|
-
}
|
|
686
519
|
function processTestSuite(testsuite) {
|
|
687
520
|
if (!testsuite)
|
|
688
521
|
return [];
|
|
@@ -694,14 +527,8 @@ function processTestSuite(testsuite) {
|
|
|
694
527
|
if (!Array.isArray(testsuite)) {
|
|
695
528
|
suites = [testsuite];
|
|
696
529
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
|
|
700
|
-
// Process child suites recursively
|
|
701
|
-
const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
|
|
702
|
-
// Process leaf suites with actual test cases
|
|
703
|
-
const leafResults = leafSuites.reduce(reduceTestCases, []);
|
|
704
|
-
return [...childResults, ...leafResults];
|
|
530
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
531
|
+
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
705
532
|
}
|
|
706
533
|
function fetchProperties(item) {
|
|
707
534
|
const tags = [];
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -56,20 +56,19 @@ function CodeceptReporter(config) {
|
|
|
56
56
|
|
|
57
57
|
output.debug = function(msg) {
|
|
58
58
|
originalOutput.debug(msg);
|
|
59
|
-
dataStorage.putData('log', repeat(this
|
|
59
|
+
dataStorage.putData('log', repeat(this.stepShift) + pc.cyan(msg.toString()));
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
output.say = function(message, color = 'cyan') {
|
|
63
63
|
originalOutput.say(message, color);
|
|
64
|
-
const sayMsg = repeat(this
|
|
64
|
+
const sayMsg = repeat(this.stepShift) + ` ${pc.bold(pc[color](message))}`;
|
|
65
65
|
dataStorage.putData('log', sayMsg);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
output.log = function(msg) {
|
|
69
69
|
originalOutput.log(msg);
|
|
70
|
-
dataStorage.putData('log', repeat(this
|
|
70
|
+
dataStorage.putData('log', repeat(this.stepShift) + pc.gray(msg));
|
|
71
71
|
};
|
|
72
|
-
output.stepShift = 0;
|
|
73
72
|
|
|
74
73
|
recorder.startUnlessRunning();
|
|
75
74
|
|
|
@@ -163,7 +162,7 @@ function CodeceptReporter(config) {
|
|
|
163
162
|
const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
|
|
164
163
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
165
164
|
const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
|
|
166
|
-
const
|
|
165
|
+
const labels = services.labels.get(test.fullTitle());
|
|
167
166
|
|
|
168
167
|
services.setContext(null);
|
|
169
168
|
|
|
@@ -178,7 +177,7 @@ function CodeceptReporter(config) {
|
|
|
178
177
|
files,
|
|
179
178
|
steps: stepHierarchy, // Array of step objects per API schema
|
|
180
179
|
logs,
|
|
181
|
-
|
|
180
|
+
labels,
|
|
182
181
|
manuallyAttachedArtifacts,
|
|
183
182
|
meta: { ...keyValues, ...test.meta },
|
|
184
183
|
});
|
package/src/adapter/mocha.js
CHANGED
|
@@ -61,7 +61,6 @@ function MochaReporter(runner, opts) {
|
|
|
61
61
|
const logs = getTestLogs(test);
|
|
62
62
|
const artifacts = services.artifacts.get(test.fullTitle());
|
|
63
63
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
64
|
-
const links = services.links.get(test.fullTitle());
|
|
65
64
|
|
|
66
65
|
client.addTestRun(STATUS.PASSED, {
|
|
67
66
|
test_id: testId,
|
|
@@ -73,7 +72,6 @@ function MochaReporter(runner, opts) {
|
|
|
73
72
|
logs,
|
|
74
73
|
manuallyAttachedArtifacts: artifacts,
|
|
75
74
|
meta: keyValues,
|
|
76
|
-
links,
|
|
77
75
|
});
|
|
78
76
|
});
|
|
79
77
|
|
|
@@ -81,10 +79,6 @@ function MochaReporter(runner, opts) {
|
|
|
81
79
|
skipped += 1;
|
|
82
80
|
console.log('skip: %s', test.fullTitle());
|
|
83
81
|
const testId = getTestomatIdFromTestTitle(test.title);
|
|
84
|
-
const artifacts = services.artifacts.get(test.fullTitle());
|
|
85
|
-
const keyValues = services.keyValues.get(test.fullTitle());
|
|
86
|
-
const links = services.links.get(test.fullTitle());
|
|
87
|
-
|
|
88
82
|
client.addTestRun(STATUS.SKIPPED, {
|
|
89
83
|
title: getTestName(test),
|
|
90
84
|
suite_title: getSuiteTitle(test),
|
|
@@ -92,9 +86,6 @@ function MochaReporter(runner, opts) {
|
|
|
92
86
|
file: getFile(test),
|
|
93
87
|
test_id: testId,
|
|
94
88
|
time: test.duration,
|
|
95
|
-
manuallyAttachedArtifacts: artifacts,
|
|
96
|
-
meta: keyValues,
|
|
97
|
-
links,
|
|
98
89
|
});
|
|
99
90
|
});
|
|
100
91
|
|
|
@@ -104,9 +95,6 @@ function MochaReporter(runner, opts) {
|
|
|
104
95
|
const testId = getTestomatIdFromTestTitle(test.title);
|
|
105
96
|
|
|
106
97
|
const logs = getTestLogs(test);
|
|
107
|
-
const artifacts = services.artifacts.get(test.fullTitle());
|
|
108
|
-
const keyValues = services.keyValues.get(test.fullTitle());
|
|
109
|
-
const links = services.links.get(test.fullTitle());
|
|
110
98
|
|
|
111
99
|
client.addTestRun(STATUS.FAILED, {
|
|
112
100
|
error: err,
|
|
@@ -117,9 +105,6 @@ function MochaReporter(runner, opts) {
|
|
|
117
105
|
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
118
106
|
time: test.duration,
|
|
119
107
|
logs,
|
|
120
|
-
manuallyAttachedArtifacts: artifacts,
|
|
121
|
-
meta: keyValues,
|
|
122
|
-
links,
|
|
123
108
|
});
|
|
124
109
|
});
|
|
125
110
|
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -53,13 +53,11 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
53
53
|
test.suite = test.parent;
|
|
54
54
|
const logs = getTestLogs(test.fullTitle);
|
|
55
55
|
// TODO: FIX: artifacts for some reason leads to empty report on Testomat.io
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const artifacts = services.artifacts.get(test.fullTitle);
|
|
59
|
-
const keyValues = services.keyValues.get(test.fullTitle);
|
|
56
|
+
// const artifacts = services.artifacts.get(test.fullTitle);
|
|
57
|
+
// const keyValues = services.keyValues.get(test.fullTitle);
|
|
60
58
|
test.logs = logs;
|
|
61
|
-
test.artifacts = artifacts;
|
|
62
|
-
test.meta = keyValues;
|
|
59
|
+
// test.artifacts = artifacts;
|
|
60
|
+
// test.meta = keyValues;
|
|
63
61
|
|
|
64
62
|
this._addTestPromises.push(this.addTest(test));
|
|
65
63
|
}
|
package/src/bin/startTest.js
CHANGED
|
@@ -1,53 +1,124 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from '
|
|
3
|
-
import {
|
|
4
|
-
import { getPackageVersion } from '../utils/utils.js';
|
|
2
|
+
import { spawn } from 'cross-spawn';
|
|
3
|
+
import { Command } from 'commander';
|
|
5
4
|
import pc from 'picocolors';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import TestomatClient from '../client.js';
|
|
6
|
+
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
7
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
10
|
|
|
11
11
|
const version = getPackageVersion();
|
|
12
12
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.option('-c, --command <cmd>', 'Test runner command')
|
|
17
|
+
.option('--launch', 'Start a new run and return its ID')
|
|
18
|
+
.option('--finish', 'Finish Run by its ID')
|
|
19
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
20
|
+
.option('--filter <filter>', 'Additional execution filter')
|
|
21
|
+
.action(async opts => {
|
|
22
|
+
const { launch, finish, filter } = opts;
|
|
23
|
+
let { command } = opts;
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (opts.envFile) dotenv.config({ path: opts.envFile });
|
|
26
|
+
|
|
27
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
28
|
+
const title = process.env.TESTOMATIO_TITLE;
|
|
29
|
+
|
|
30
|
+
if (launch) {
|
|
31
|
+
console.log('Starting a new Run on Testomat.io...');
|
|
32
|
+
const client = new TestomatClient({ apiKey });
|
|
33
|
+
|
|
34
|
+
client.createRun().then(() => {
|
|
35
|
+
console.log(process.env.runId);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (finish) {
|
|
42
|
+
// TODO: add error in case of TESTOMATIO environment variable is not set
|
|
43
|
+
// because command is fine in console, but actually (on testomat.io) run is not finished
|
|
44
|
+
if (!process.env.TESTOMATIO_RUN) {
|
|
45
|
+
console.log('TESTOMATIO_RUN environment variable must be set.');
|
|
46
|
+
return process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('Finishing Run on Testomat.io...');
|
|
50
|
+
|
|
51
|
+
const client = new TestomatClient({ apiKey });
|
|
52
|
+
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
client.updateRunStatus(STATUS.FINISHED).then(() => {
|
|
55
|
+
console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let exitCode = 0;
|
|
62
|
+
|
|
63
|
+
if (!command.split) {
|
|
64
|
+
process.exitCode = 255;
|
|
65
|
+
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
66
|
+
return;
|
|
27
67
|
}
|
|
28
|
-
} else if (arg.startsWith('--command=')) {
|
|
29
|
-
// Handle --command=value format
|
|
30
|
-
const command = arg.split('=', 2)[1];
|
|
31
|
-
newArgs.push(command);
|
|
32
|
-
} else if (arg === '--launch') {
|
|
33
|
-
// Map --launch to start command
|
|
34
|
-
newArgs[0] = 'start';
|
|
35
|
-
} else if (arg === '--finish') {
|
|
36
|
-
// Map --finish to finish command
|
|
37
|
-
newArgs[0] = 'finish';
|
|
38
|
-
} else {
|
|
39
|
-
// Pass through other arguments
|
|
40
|
-
newArgs.push(arg);
|
|
41
|
-
}
|
|
42
|
-
i++;
|
|
43
|
-
}
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
if (filter) {
|
|
72
|
+
const [pipe, ...optsArray] = filter.split(':');
|
|
73
|
+
const pipeOptions = optsArray.join(':');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const tests = await client.prepareRun({ pipe, pipeOptions });
|
|
77
|
+
|
|
78
|
+
if (!tests || tests.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const grep = ` --grep (${tests.join('|')})`;
|
|
83
|
+
command += grep;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.log(APP_PREFIX, err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const testCmds = command.split(' ');
|
|
90
|
+
console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
|
|
91
|
+
|
|
92
|
+
if (!apiKey) {
|
|
93
|
+
const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
|
|
94
|
+
|
|
95
|
+
cmd.on('close', code => {
|
|
96
|
+
console.log(APP_PREFIX, '⚠️ ', `Runner exited with ${pc.bold(code)}, report is ignored`);
|
|
97
|
+
|
|
98
|
+
if (code > exitCode) exitCode = code;
|
|
99
|
+
process.exitCode = exitCode;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
client.createRun().then(() => {
|
|
106
|
+
const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
|
|
107
|
+
|
|
108
|
+
cmd.on('close', code => {
|
|
109
|
+
const emoji = code === 0 ? '🟢' : '🔴';
|
|
110
|
+
console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
|
|
111
|
+
const status = code === 0 ? 'passed' : 'failed';
|
|
112
|
+
client.updateRunStatus(status, true);
|
|
113
|
+
|
|
114
|
+
if (code > exitCode) exitCode = code;
|
|
115
|
+
process.exitCode = exitCode;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (process.argv.length <= 2) {
|
|
121
|
+
program.outputHelp();
|
|
122
|
+
}
|
|
50
123
|
|
|
51
|
-
|
|
52
|
-
process.exit(code);
|
|
53
|
-
});
|
|
124
|
+
program.parse(process.argv);
|
package/src/client.js
CHANGED
|
@@ -11,7 +11,6 @@ import path, { sep } from 'path';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { S3Uploader } from './uploader.js';
|
|
13
13
|
import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
|
|
14
|
-
import { linkStorage } from './services/links.js';
|
|
15
14
|
import { filesize as prettyBytes } from 'filesize';
|
|
16
15
|
|
|
17
16
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
@@ -183,6 +182,7 @@ class Client {
|
|
|
183
182
|
test_id,
|
|
184
183
|
timestamp,
|
|
185
184
|
manuallyAttachedArtifacts,
|
|
185
|
+
labels,
|
|
186
186
|
overwrite,
|
|
187
187
|
} = testData;
|
|
188
188
|
let { message = '', meta = {} } = testData;
|
|
@@ -224,9 +224,7 @@ class Client {
|
|
|
224
224
|
return acc;
|
|
225
225
|
}, {});
|
|
226
226
|
|
|
227
|
-
//
|
|
228
|
-
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
229
|
-
const links = linkStorage.get(testContext) || [];
|
|
227
|
+
// Labels are simple array of strings, no processing needed
|
|
230
228
|
|
|
231
229
|
let errorFormatted = '';
|
|
232
230
|
if (error) {
|
|
@@ -282,7 +280,7 @@ class Client {
|
|
|
282
280
|
timestamp,
|
|
283
281
|
artifacts,
|
|
284
282
|
meta,
|
|
285
|
-
|
|
283
|
+
labels,
|
|
286
284
|
overwrite,
|
|
287
285
|
...(rootSuiteId && { root_suite_id: rootSuiteId }),
|
|
288
286
|
};
|
package/src/data-storage.js
CHANGED
|
@@ -41,7 +41,7 @@ class DataStorage {
|
|
|
41
41
|
/**
|
|
42
42
|
* Puts any data to storage (file or global variable).
|
|
43
43
|
* If file: stores data as text, if global variable – stores as array of data.
|
|
44
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
44
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
45
45
|
* @param {*} data anything you want to store (string, object, array, etc)
|
|
46
46
|
* @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
|
|
47
47
|
* suite name + test name is used by default
|
|
@@ -70,7 +70,7 @@ class DataStorage {
|
|
|
70
70
|
* Returns data, stored for specific test/context (or data which was stored without test id specified).
|
|
71
71
|
* This method will get data from global variable and/or from from file (previosly saved with put method).
|
|
72
72
|
*
|
|
73
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
73
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
74
74
|
* @param {string} context
|
|
75
75
|
* @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
|
|
76
76
|
*/
|
|
@@ -108,7 +108,7 @@ class DataStorage {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
111
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
112
112
|
* @param {string} context
|
|
113
113
|
* @returns aray of data (any type)
|
|
114
114
|
*/
|
|
@@ -127,7 +127,7 @@ class DataStorage {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
130
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
131
131
|
* @param {*} context
|
|
132
132
|
* @returns array of data (any type)
|
|
133
133
|
*/
|
|
@@ -151,7 +151,7 @@ class DataStorage {
|
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
154
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
154
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
155
155
|
* @param {*} data
|
|
156
156
|
* @param {*} context
|
|
157
157
|
*/
|
|
@@ -166,7 +166,7 @@ class DataStorage {
|
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* Puts data to file. Unlike the global variable storage, stores data as string
|
|
169
|
-
* @param {'log' | 'artifact' | 'keyvalue' | '
|
|
169
|
+
* @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
|
|
170
170
|
* @param {*} data
|
|
171
171
|
* @param {string} context
|
|
172
172
|
* @returns
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -119,7 +119,8 @@ class TestomatioPipe {
|
|
|
119
119
|
const resp = await this.client.request({
|
|
120
120
|
method: 'GET',
|
|
121
121
|
url: '/api/test_grep',
|
|
122
|
-
|
|
122
|
+
params: q.params,
|
|
123
|
+
responseType: q.responseType
|
|
123
124
|
});
|
|
124
125
|
|
|
125
126
|
if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
|