@testomatio/reporter 2.1.1 → 2.1.3-beta.1-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 +2 -2
- package/lib/adapter/mocha.js +14 -0
- package/lib/adapter/webdriver.js +6 -4
- package/lib/bin/startTest.js +38 -91
- package/lib/client.js +6 -3
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/reporter-functions.d.ts +10 -3
- package/lib/reporter-functions.js +18 -28
- package/lib/reporter.d.ts +16 -14
- package/lib/reporter.js +3 -1
- 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/links.d.ts +22 -0
- package/lib/services/links.js +71 -0
- package/lib/services/logger.d.ts +1 -1
- package/lib/xmlReader.d.ts +7 -0
- package/lib/xmlReader.js +180 -7
- package/package.json +1 -1
- package/src/adapter/codecept.js +2 -2
- package/src/adapter/mocha.js +15 -0
- package/src/adapter/webdriver.js +6 -4
- package/src/bin/startTest.js +43 -114
- package/src/client.js +5 -3
- package/src/data-storage.js +6 -6
- package/src/reporter-functions.js +18 -29
- package/src/reporter.js +2 -0
- package/src/services/index.js +2 -2
- package/src/services/labels.js +2 -2
- package/src/services/links.js +69 -0
- package/src/xmlReader.js +210 -7
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const linkStorage: LinkStorage;
|
|
2
|
+
declare class LinkStorage {
|
|
3
|
+
static "__#13@#instance": any;
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @returns {LinkStorage}
|
|
7
|
+
*/
|
|
8
|
+
static getInstance(): LinkStorage;
|
|
9
|
+
/**
|
|
10
|
+
* Stores links array and passes it to reporter
|
|
11
|
+
* @param {object[]} links - array of link objects
|
|
12
|
+
* @param {*} context - full test title
|
|
13
|
+
*/
|
|
14
|
+
put(links: object[], context?: any): void;
|
|
15
|
+
/**
|
|
16
|
+
* Returns links array for the test
|
|
17
|
+
* @param {*} context testId or test context from test runner
|
|
18
|
+
* @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
|
|
19
|
+
*/
|
|
20
|
+
get(context?: any): object[];
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.linkStorage = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const data_storage_js_1 = require("../data-storage.js");
|
|
9
|
+
const debug = (0, debug_1.default)('@testomatio/reporter:services-links');
|
|
10
|
+
class LinkStorage {
|
|
11
|
+
static #instance;
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @returns {LinkStorage}
|
|
15
|
+
*/
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!this.#instance) {
|
|
18
|
+
this.#instance = new LinkStorage();
|
|
19
|
+
}
|
|
20
|
+
return this.#instance;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Stores links array and passes it to reporter
|
|
24
|
+
* @param {object[]} links - array of link objects
|
|
25
|
+
* @param {*} context - full test title
|
|
26
|
+
*/
|
|
27
|
+
put(links, context = null) {
|
|
28
|
+
if (!links || !Array.isArray(links))
|
|
29
|
+
return;
|
|
30
|
+
data_storage_js_1.dataStorage.putData('links', links, context);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns links array for the test
|
|
34
|
+
* @param {*} context testId or test context from test runner
|
|
35
|
+
* @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
|
|
36
|
+
*/
|
|
37
|
+
get(context = null) {
|
|
38
|
+
const linksList = data_storage_js_1.dataStorage.getData('links', context);
|
|
39
|
+
if (!linksList || !linksList?.length)
|
|
40
|
+
return [];
|
|
41
|
+
const allLinks = [];
|
|
42
|
+
for (const links of linksList) {
|
|
43
|
+
if (Array.isArray(links)) {
|
|
44
|
+
allLinks.push(...links);
|
|
45
|
+
}
|
|
46
|
+
else if (typeof links === 'string') {
|
|
47
|
+
try {
|
|
48
|
+
const parsedLinks = JSON.parse(links);
|
|
49
|
+
if (Array.isArray(parsedLinks)) {
|
|
50
|
+
allLinks.push(...parsedLinks);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
debug(`Error parsing links for test ${context}`, links);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Remove duplicates based on JSON string comparison
|
|
59
|
+
const uniqueLinks = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
for (const link of allLinks) {
|
|
62
|
+
const key = JSON.stringify(link);
|
|
63
|
+
if (!seen.has(key)) {
|
|
64
|
+
seen.add(key);
|
|
65
|
+
uniqueLinks.push(link);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return uniqueLinks;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.linkStorage = LinkStorage.getInstance();
|
package/lib/services/logger.d.ts
CHANGED
package/lib/xmlReader.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ 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;
|
|
80
87
|
calculateStats(): {};
|
|
81
88
|
fetchSourceCode(): void;
|
|
82
89
|
formatTests(): void;
|
package/lib/xmlReader.js
CHANGED
|
@@ -131,7 +131,9 @@ 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
|
-
|
|
134
|
+
// Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
|
|
135
|
+
const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
|
|
136
|
+
this.tests = this.tests.concat(deduplicatedTests);
|
|
135
137
|
return {
|
|
136
138
|
status: result?.toLowerCase(),
|
|
137
139
|
create_tests: true,
|
|
@@ -139,7 +141,7 @@ class XmlReader {
|
|
|
139
141
|
passed_count: parseInt(passed, 10),
|
|
140
142
|
failed_count: parseInt(failed, 10),
|
|
141
143
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
142
|
-
tests:
|
|
144
|
+
tests: deduplicatedTests,
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
processTRX(jsonSuite) {
|
|
@@ -275,6 +277,125 @@ class XmlReader {
|
|
|
275
277
|
tests,
|
|
276
278
|
};
|
|
277
279
|
}
|
|
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
|
+
}
|
|
278
399
|
calculateStats() {
|
|
279
400
|
this.stats = {
|
|
280
401
|
...this.stats,
|
|
@@ -430,7 +551,8 @@ function reduceTestCases(prev, item) {
|
|
|
430
551
|
testCases
|
|
431
552
|
.filter(t => !!t)
|
|
432
553
|
.forEach(testCaseItem => {
|
|
433
|
-
|
|
554
|
+
// Use consistent Test Explorer structure: prioritize fullname for file path
|
|
555
|
+
const file = extractSourceFilePath(testCaseItem, item);
|
|
434
556
|
let stack = '';
|
|
435
557
|
let message = '';
|
|
436
558
|
if (testCaseItem.error)
|
|
@@ -450,11 +572,11 @@ function reduceTestCases(prev, item) {
|
|
|
450
572
|
if (!message)
|
|
451
573
|
message = stack.trim().split('\n')[0];
|
|
452
574
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
453
|
-
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
454
575
|
// SpecFlow config
|
|
455
576
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
456
577
|
let example = null;
|
|
457
|
-
|
|
578
|
+
// Use consistent Test Explorer structure for suite title
|
|
579
|
+
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
458
580
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
459
581
|
tags ||= [];
|
|
460
582
|
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
@@ -516,6 +638,51 @@ function reduceTestCases(prev, item) {
|
|
|
516
638
|
});
|
|
517
639
|
return prev;
|
|
518
640
|
}
|
|
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
|
+
}
|
|
519
686
|
function processTestSuite(testsuite) {
|
|
520
687
|
if (!testsuite)
|
|
521
688
|
return [];
|
|
@@ -527,8 +694,14 @@ function processTestSuite(testsuite) {
|
|
|
527
694
|
if (!Array.isArray(testsuite)) {
|
|
528
695
|
suites = [testsuite];
|
|
529
696
|
}
|
|
530
|
-
|
|
531
|
-
|
|
697
|
+
// Only process suites that have test cases OR child suites, but avoid double processing
|
|
698
|
+
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
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];
|
|
532
705
|
}
|
|
533
706
|
function fetchProperties(item) {
|
|
534
707
|
const tags = [];
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -163,7 +163,7 @@ function CodeceptReporter(config) {
|
|
|
163
163
|
const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
|
|
164
164
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
165
165
|
const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
|
|
166
|
-
const
|
|
166
|
+
const links = services.links.get(test.fullTitle());
|
|
167
167
|
|
|
168
168
|
services.setContext(null);
|
|
169
169
|
|
|
@@ -178,7 +178,7 @@ function CodeceptReporter(config) {
|
|
|
178
178
|
files,
|
|
179
179
|
steps: stepHierarchy, // Array of step objects per API schema
|
|
180
180
|
logs,
|
|
181
|
-
|
|
181
|
+
links,
|
|
182
182
|
manuallyAttachedArtifacts,
|
|
183
183
|
meta: { ...keyValues, ...test.meta },
|
|
184
184
|
});
|
package/src/adapter/mocha.js
CHANGED
|
@@ -61,6 +61,7 @@ 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());
|
|
64
65
|
|
|
65
66
|
client.addTestRun(STATUS.PASSED, {
|
|
66
67
|
test_id: testId,
|
|
@@ -72,6 +73,7 @@ function MochaReporter(runner, opts) {
|
|
|
72
73
|
logs,
|
|
73
74
|
manuallyAttachedArtifacts: artifacts,
|
|
74
75
|
meta: keyValues,
|
|
76
|
+
links,
|
|
75
77
|
});
|
|
76
78
|
});
|
|
77
79
|
|
|
@@ -79,6 +81,10 @@ function MochaReporter(runner, opts) {
|
|
|
79
81
|
skipped += 1;
|
|
80
82
|
console.log('skip: %s', test.fullTitle());
|
|
81
83
|
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
|
+
|
|
82
88
|
client.addTestRun(STATUS.SKIPPED, {
|
|
83
89
|
title: getTestName(test),
|
|
84
90
|
suite_title: getSuiteTitle(test),
|
|
@@ -86,6 +92,9 @@ function MochaReporter(runner, opts) {
|
|
|
86
92
|
file: getFile(test),
|
|
87
93
|
test_id: testId,
|
|
88
94
|
time: test.duration,
|
|
95
|
+
manuallyAttachedArtifacts: artifacts,
|
|
96
|
+
meta: keyValues,
|
|
97
|
+
links,
|
|
89
98
|
});
|
|
90
99
|
});
|
|
91
100
|
|
|
@@ -95,6 +104,9 @@ function MochaReporter(runner, opts) {
|
|
|
95
104
|
const testId = getTestomatIdFromTestTitle(test.title);
|
|
96
105
|
|
|
97
106
|
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());
|
|
98
110
|
|
|
99
111
|
client.addTestRun(STATUS.FAILED, {
|
|
100
112
|
error: err,
|
|
@@ -105,6 +117,9 @@ function MochaReporter(runner, opts) {
|
|
|
105
117
|
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
106
118
|
time: test.duration,
|
|
107
119
|
logs,
|
|
120
|
+
manuallyAttachedArtifacts: artifacts,
|
|
121
|
+
meta: keyValues,
|
|
122
|
+
links,
|
|
108
123
|
});
|
|
109
124
|
});
|
|
110
125
|
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -53,11 +53,13 @@ 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
|
-
//
|
|
56
|
+
// ^ not reproduced anymore (Jul 2025)
|
|
57
|
+
// but still be under investigation
|
|
58
|
+
const artifacts = services.artifacts.get(test.fullTitle);
|
|
59
|
+
const keyValues = services.keyValues.get(test.fullTitle);
|
|
58
60
|
test.logs = logs;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
test.artifacts = artifacts;
|
|
62
|
+
test.meta = keyValues;
|
|
61
63
|
|
|
62
64
|
this._addTestPromises.push(this.addTest(test));
|
|
63
65
|
}
|
package/src/bin/startTest.js
CHANGED
|
@@ -1,124 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from '
|
|
3
|
-
import {
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import TestomatClient from '../client.js';
|
|
6
|
-
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
7
4
|
import { getPackageVersion } from '../utils/utils.js';
|
|
8
|
-
import
|
|
9
|
-
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
// Define __dirname - this will be replaced by build script with actual __dirname for CommonJS
|
|
8
|
+
const __dirname = typeof globalThis.__dirname !== 'undefined' ? globalThis.__dirname : '.';
|
|
9
|
+
const cliPath = join(__dirname, 'cli.js');
|
|
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;
|
|
24
|
-
|
|
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
13
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
14
|
+
// Parse command line arguments to map start-test-run options to @testomatio/reporter run format
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const newArgs = ['run'];
|
|
17
|
+
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < args.length) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
|
|
22
|
+
if (arg === '-c' || arg === '--command') {
|
|
23
|
+
// Map -c/--command to positional argument for run command
|
|
24
|
+
i++;
|
|
25
|
+
if (i < args.length) {
|
|
26
|
+
newArgs.push(args[i]);
|
|
59
27
|
}
|
|
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
|
+
}
|
|
60
44
|
|
|
61
|
-
|
|
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;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
70
|
-
|
|
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
|
-
});
|
|
45
|
+
// Execute the main CLI with mapped arguments
|
|
119
46
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
47
|
+
const child = spawn(process.execPath, [cliPath, ...newArgs], {
|
|
48
|
+
stdio: 'inherit'
|
|
49
|
+
});
|
|
123
50
|
|
|
124
|
-
|
|
51
|
+
child.on('exit', (code) => {
|
|
52
|
+
process.exit(code);
|
|
53
|
+
});
|
package/src/client.js
CHANGED
|
@@ -11,6 +11,7 @@ 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';
|
|
14
15
|
import { filesize as prettyBytes } from 'filesize';
|
|
15
16
|
|
|
16
17
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
@@ -182,7 +183,6 @@ class Client {
|
|
|
182
183
|
test_id,
|
|
183
184
|
timestamp,
|
|
184
185
|
manuallyAttachedArtifacts,
|
|
185
|
-
labels,
|
|
186
186
|
overwrite,
|
|
187
187
|
} = testData;
|
|
188
188
|
let { message = '', meta = {} } = testData;
|
|
@@ -224,7 +224,9 @@ class Client {
|
|
|
224
224
|
return acc;
|
|
225
225
|
}, {});
|
|
226
226
|
|
|
227
|
-
//
|
|
227
|
+
// Get links from storage using the test context
|
|
228
|
+
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
229
|
+
const links = linkStorage.get(testContext) || [];
|
|
228
230
|
|
|
229
231
|
let errorFormatted = '';
|
|
230
232
|
if (error) {
|
|
@@ -280,7 +282,7 @@ class Client {
|
|
|
280
282
|
timestamp,
|
|
281
283
|
artifacts,
|
|
282
284
|
meta,
|
|
283
|
-
|
|
285
|
+
links,
|
|
284
286
|
overwrite,
|
|
285
287
|
...(rootSuiteId && { root_suite_id: rootSuiteId }),
|
|
286
288
|
};
|