@testomatio/reporter 2.9.3-beta.4-allure-links → 2.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/lib/adapter/codecept.js +33 -14
- package/lib/adapter/playwright.js +19 -8
- package/lib/bin/cli.js +68 -34
- package/lib/client.js +4 -3
- package/lib/junit-adapter/index.js +0 -4
- package/lib/pipe/debug.d.ts +6 -0
- package/lib/pipe/debug.js +11 -0
- package/lib/pipe/html.d.ts +1 -0
- package/lib/pipe/html.js +152 -45
- package/lib/replay.js +21 -0
- package/lib/template/testomatio.hbs +77 -11
- package/lib/utils/pipe_utils.d.ts +0 -17
- package/lib/utils/pipe_utils.js +0 -45
- package/lib/utils/utils.js +0 -9
- package/lib/xmlReader.d.ts +1 -0
- package/lib/xmlReader.js +38 -2
- package/package.json +1 -1
- package/src/adapter/codecept.js +35 -12
- package/src/adapter/playwright.js +22 -9
- package/src/bin/cli.js +74 -43
- package/src/client.js +4 -3
- package/src/junit-adapter/index.js +0 -4
- package/src/pipe/debug.js +11 -0
- package/src/pipe/html.js +171 -52
- package/src/replay.js +21 -0
- package/src/template/testomatio.hbs +77 -11
- package/src/utils/pipe_utils.js +0 -49
- package/src/utils/utils.js +0 -5
- package/src/xmlReader.js +47 -2
- package/lib/allureReader.d.ts +0 -217
- package/lib/allureReader.js +0 -860
- package/lib/junit-adapter/kotlin.d.ts +0 -5
- package/lib/junit-adapter/kotlin.js +0 -46
- package/src/allureReader.js +0 -954
- package/src/junit-adapter/kotlin.js +0 -48
|
@@ -681,8 +681,9 @@
|
|
|
681
681
|
|
|
682
682
|
.test-group-header {
|
|
683
683
|
padding: 20px;
|
|
684
|
-
background:
|
|
685
|
-
color:
|
|
684
|
+
background: var(--gray-50);
|
|
685
|
+
color: var(--gray-800);
|
|
686
|
+
border-left: 4px solid var(--primary-color);
|
|
686
687
|
cursor: pointer;
|
|
687
688
|
display: flex;
|
|
688
689
|
justify-content: space-between;
|
|
@@ -691,7 +692,37 @@
|
|
|
691
692
|
}
|
|
692
693
|
|
|
693
694
|
.test-group-header:hover {
|
|
694
|
-
|
|
695
|
+
background: var(--gray-100);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.test-group-header.has-failures {
|
|
699
|
+
background: #fef2f2;
|
|
700
|
+
color: var(--gray-900);
|
|
701
|
+
border-left-color: var(--danger-color);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.test-group-header.has-failures:hover {
|
|
705
|
+
background: #fee2e2;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.test-group-header.has-skips {
|
|
709
|
+
background: #fffbeb;
|
|
710
|
+
color: var(--gray-900);
|
|
711
|
+
border-left-color: var(--warning-color);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.test-group-header.has-skips:hover {
|
|
715
|
+
background: #fef3c7;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.test-group-header.all-passed {
|
|
719
|
+
background: #ecfdf5;
|
|
720
|
+
color: var(--gray-900);
|
|
721
|
+
border-left-color: var(--success-color);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.test-group-header.all-passed:hover {
|
|
725
|
+
background: #d1fae5;
|
|
695
726
|
}
|
|
696
727
|
|
|
697
728
|
.test-group-title {
|
|
@@ -707,28 +738,56 @@
|
|
|
707
738
|
}
|
|
708
739
|
|
|
709
740
|
.test-group-count {
|
|
710
|
-
background:
|
|
741
|
+
background: white;
|
|
742
|
+
color: var(--gray-600);
|
|
711
743
|
padding: 6px 16px;
|
|
712
744
|
border-radius: 20px;
|
|
713
745
|
font-size: 14px;
|
|
714
746
|
font-weight: 600;
|
|
747
|
+
border: 1px solid var(--gray-200);
|
|
715
748
|
}
|
|
716
749
|
|
|
717
750
|
.test-group-stats {
|
|
718
751
|
display: flex;
|
|
719
|
-
gap:
|
|
752
|
+
gap: 8px;
|
|
720
753
|
font-size: 14px;
|
|
721
754
|
margin-right: 10px;
|
|
755
|
+
flex-wrap: wrap;
|
|
756
|
+
justify-content: flex-end;
|
|
722
757
|
}
|
|
723
758
|
|
|
724
759
|
.test-group-stat {
|
|
725
760
|
display: flex;
|
|
726
761
|
align-items: center;
|
|
727
762
|
gap: 6px;
|
|
763
|
+
padding: 6px 10px;
|
|
764
|
+
border-radius: 999px;
|
|
765
|
+
font-weight: 700;
|
|
766
|
+
background: white;
|
|
767
|
+
border: 1px solid currentColor;
|
|
728
768
|
}
|
|
729
769
|
|
|
730
|
-
.test-group-
|
|
770
|
+
.test-group-stat.passed {
|
|
771
|
+
color: var(--success-color);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.test-group-stat.failed {
|
|
731
775
|
color: white;
|
|
776
|
+
background: var(--danger-color);
|
|
777
|
+
border-color: var(--danger-color);
|
|
778
|
+
box-shadow: 0 1px 3px rgba(239, 68, 68, 0.35);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.test-group-stat.skipped {
|
|
782
|
+
color: var(--warning-color);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.test-group-stat.todo {
|
|
786
|
+
color: #8b5cf6;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.test-group-expand {
|
|
790
|
+
color: var(--gray-500);
|
|
732
791
|
transition: var(--transition);
|
|
733
792
|
font-size: 18px;
|
|
734
793
|
}
|
|
@@ -3366,11 +3425,18 @@
|
|
|
3366
3425
|
const failedCount = group.tests.filter(t => t.status.toLowerCase() === 'failed').length;
|
|
3367
3426
|
const skippedCount = group.tests.filter(t => t.status.toLowerCase() === 'skipped').length;
|
|
3368
3427
|
const todoCount = group.tests.filter(t => t.status.toLowerCase() === 'todo').length;
|
|
3428
|
+
const groupStateClass = failedCount > 0
|
|
3429
|
+
? 'has-failures'
|
|
3430
|
+
: skippedCount > 0 || todoCount > 0
|
|
3431
|
+
? 'has-skips'
|
|
3432
|
+
: passedCount > 0
|
|
3433
|
+
? 'all-passed'
|
|
3434
|
+
: '';
|
|
3369
3435
|
|
|
3370
3436
|
const groupIcon = group.type === 'suite' ? 'layer-group' : 'file-code';
|
|
3371
3437
|
|
|
3372
3438
|
const header = document.createElement('div');
|
|
3373
|
-
header.className = 'test-group-header';
|
|
3439
|
+
header.className = ['test-group-header', groupStateClass].filter(Boolean).join(' ');
|
|
3374
3440
|
header.innerHTML = `
|
|
3375
3441
|
<div class='test-group-title'>
|
|
3376
3442
|
<i class='fas fa-${groupIcon}'></i>
|
|
@@ -3378,10 +3444,10 @@
|
|
|
3378
3444
|
<div class='test-group-count'>${group.tests.length} tests</div>
|
|
3379
3445
|
</div>
|
|
3380
3446
|
<div class='test-group-stats'>
|
|
3381
|
-
${passedCount > 0 ? `<div class='test-group-stat
|
|
3382
|
-
${failedCount > 0 ? `<div class='test-group-stat
|
|
3383
|
-
${skippedCount > 0 ? `<div class='test-group-stat
|
|
3384
|
-
${todoCount > 0 ? `<div class='test-group-stat
|
|
3447
|
+
${passedCount > 0 ? `<div class='test-group-stat passed'><i class='fas fa-check-circle'></i> ${passedCount}</div>` : ''}
|
|
3448
|
+
${failedCount > 0 ? `<div class='test-group-stat failed'><i class='fas fa-times-circle'></i> ${failedCount}</div>` : ''}
|
|
3449
|
+
${skippedCount > 0 ? `<div class='test-group-stat skipped'><i class='fas fa-forward'></i> ${skippedCount}</div>` : ''}
|
|
3450
|
+
${todoCount > 0 ? `<div class='test-group-stat todo'><i class='fas fa-clock'></i> ${todoCount}</div>` : ''}
|
|
3385
3451
|
</div>
|
|
3386
3452
|
<i class='fas fa-chevron-down test-group-expand'></i>
|
|
3387
3453
|
`;
|
|
@@ -64,20 +64,3 @@ export function parsePipeOptions(optionsStr?: string): any;
|
|
|
64
64
|
* @returns {string} Empty string if no ids; otherwise the formatted output.
|
|
65
65
|
*/
|
|
66
66
|
export function formatFilterListIds(ids: string[], format: "grep" | "json" | "newline" | "ids"): string;
|
|
67
|
-
/**
|
|
68
|
-
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
69
|
-
* @param {Object} data - Data to measure
|
|
70
|
-
* @returns {number} Size in bytes
|
|
71
|
-
*/
|
|
72
|
-
export function getObjectSize(data: any): number;
|
|
73
|
-
/**
|
|
74
|
-
* Split a tests array into chunks bounded by serialized size.
|
|
75
|
-
* Used by the XML and Allure readers so both upload tests with identical batching:
|
|
76
|
-
* each chunk becomes a single batch request (manual batch mode), which keeps requests
|
|
77
|
-
* under the size limit and guarantees every test is sent exactly once.
|
|
78
|
-
*
|
|
79
|
-
* @param {Array} tests - Array of tests to split
|
|
80
|
-
* @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
|
|
81
|
-
* @returns {Array<Array>} Array of test chunks
|
|
82
|
-
*/
|
|
83
|
-
export function splitTestsIntoChunks(tests: any[], maxSizeBytes?: number): Array<any[]>;
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -8,8 +8,6 @@ exports.statusEmoji = statusEmoji;
|
|
|
8
8
|
exports.fullName = fullName;
|
|
9
9
|
exports.parsePipeOptions = parsePipeOptions;
|
|
10
10
|
exports.formatFilterListIds = formatFilterListIds;
|
|
11
|
-
exports.getObjectSize = getObjectSize;
|
|
12
|
-
exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
|
13
11
|
const log_js_1 = require("./log.js");
|
|
14
12
|
/**
|
|
15
13
|
* Set S3 credentials from the provided artifacts object.
|
|
@@ -164,45 +162,6 @@ function parsePipeOptions(optionsStr) {
|
|
|
164
162
|
}
|
|
165
163
|
return options;
|
|
166
164
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
169
|
-
* @param {Object} data - Data to measure
|
|
170
|
-
* @returns {number} Size in bytes
|
|
171
|
-
*/
|
|
172
|
-
function getObjectSize(data) {
|
|
173
|
-
const body = JSON.stringify(data);
|
|
174
|
-
return new TextEncoder().encode(body).length;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Split a tests array into chunks bounded by serialized size.
|
|
178
|
-
* Used by the XML and Allure readers so both upload tests with identical batching:
|
|
179
|
-
* each chunk becomes a single batch request (manual batch mode), which keeps requests
|
|
180
|
-
* under the size limit and guarantees every test is sent exactly once.
|
|
181
|
-
*
|
|
182
|
-
* @param {Array} tests - Array of tests to split
|
|
183
|
-
* @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
|
|
184
|
-
* @returns {Array<Array>} Array of test chunks
|
|
185
|
-
*/
|
|
186
|
-
function splitTestsIntoChunks(tests, maxSizeBytes = 1 * 1024 * 1024) {
|
|
187
|
-
const chunks = [];
|
|
188
|
-
let currentChunk = [];
|
|
189
|
-
let currentChunkSize = 0;
|
|
190
|
-
for (const test of tests) {
|
|
191
|
-
const testSize = getObjectSize(test);
|
|
192
|
-
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
193
|
-
if (wouldExceedSize && currentChunk.length > 0) {
|
|
194
|
-
chunks.push(currentChunk);
|
|
195
|
-
currentChunk = [];
|
|
196
|
-
currentChunkSize = 0;
|
|
197
|
-
}
|
|
198
|
-
currentChunk.push(test);
|
|
199
|
-
currentChunkSize += testSize;
|
|
200
|
-
}
|
|
201
|
-
if (currentChunk.length > 0) {
|
|
202
|
-
chunks.push(currentChunk);
|
|
203
|
-
}
|
|
204
|
-
return chunks;
|
|
205
|
-
}
|
|
206
165
|
/**
|
|
207
166
|
* Format a list of test IDs for `--filter-list` machine-readable output.
|
|
208
167
|
* Used when the CLI `--format` option is passed,
|
|
@@ -239,7 +198,3 @@ module.exports.fullName = fullName;
|
|
|
239
198
|
module.exports.parsePipeOptions = parsePipeOptions;
|
|
240
199
|
|
|
241
200
|
module.exports.formatFilterListIds = formatFilterListIds;
|
|
242
|
-
|
|
243
|
-
module.exports.getObjectSize = getObjectSize;
|
|
244
|
-
|
|
245
|
-
module.exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
package/lib/utils/utils.js
CHANGED
|
@@ -324,15 +324,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
324
324
|
if (lineIndex === -1)
|
|
325
325
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
326
326
|
}
|
|
327
|
-
else if (opts.lang === 'kotlin') {
|
|
328
|
-
lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
|
|
329
|
-
if (lineIndex === -1)
|
|
330
|
-
lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
331
|
-
if (lineIndex === -1)
|
|
332
|
-
lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
|
|
333
|
-
if (lineIndex === -1)
|
|
334
|
-
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
335
|
-
}
|
|
336
327
|
else if (opts.lang === 'csharp') {
|
|
337
328
|
// Find the method declaration line
|
|
338
329
|
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
package/lib/xmlReader.d.ts
CHANGED
package/lib/xmlReader.js
CHANGED
|
@@ -14,7 +14,6 @@ const url_1 = require("url");
|
|
|
14
14
|
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
15
15
|
const utils_js_1 = require("./utils/utils.js");
|
|
16
16
|
const index_js_1 = require("./pipe/index.js");
|
|
17
|
-
const pipe_utils_js_1 = require("./utils/pipe_utils.js");
|
|
18
17
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
19
18
|
const config_js_1 = require("./config.js");
|
|
20
19
|
const uploader_js_1 = require("./uploader.js");
|
|
@@ -475,6 +474,43 @@ class XmlReader {
|
|
|
475
474
|
this.uploader.checkEnabled();
|
|
476
475
|
return run;
|
|
477
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Calculate the approximate size of data in bytes (JSON stringified length)
|
|
479
|
+
* @param {Object} data - Data to measure
|
|
480
|
+
* @returns {number} Size in bytes
|
|
481
|
+
*/
|
|
482
|
+
#getObjectSize(data) {
|
|
483
|
+
const body = JSON.stringify(data);
|
|
484
|
+
return new TextEncoder().encode(body).length;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Split tests array into chunks based on data size
|
|
488
|
+
* @param {Array} tests - Array of tests to split
|
|
489
|
+
* @returns {Array<Array>} Array of test chunks
|
|
490
|
+
*/
|
|
491
|
+
#splitTestsIntoChunks(tests) {
|
|
492
|
+
const maxSizeBytes = 1 * 1024 * 1024;
|
|
493
|
+
const chunks = [];
|
|
494
|
+
let currentChunk = [];
|
|
495
|
+
let currentChunkSize = 0;
|
|
496
|
+
for (const test of tests) {
|
|
497
|
+
const testSize = this.#getObjectSize(test);
|
|
498
|
+
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
499
|
+
if (wouldExceedSize) {
|
|
500
|
+
if (currentChunk.length > 0) {
|
|
501
|
+
chunks.push(currentChunk);
|
|
502
|
+
}
|
|
503
|
+
currentChunk = [];
|
|
504
|
+
currentChunkSize = 0;
|
|
505
|
+
}
|
|
506
|
+
currentChunk.push(test);
|
|
507
|
+
currentChunkSize += testSize;
|
|
508
|
+
}
|
|
509
|
+
if (currentChunk.length > 0) {
|
|
510
|
+
chunks.push(currentChunk);
|
|
511
|
+
}
|
|
512
|
+
return chunks;
|
|
513
|
+
}
|
|
478
514
|
async uploadData() {
|
|
479
515
|
await this.uploadArtifacts();
|
|
480
516
|
this.calculateStats();
|
|
@@ -495,7 +531,7 @@ class XmlReader {
|
|
|
495
531
|
};
|
|
496
532
|
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
497
533
|
}
|
|
498
|
-
const testChunks =
|
|
534
|
+
const testChunks = this.#splitTestsIntoChunks(this.tests);
|
|
499
535
|
const totalChunks = testChunks.length;
|
|
500
536
|
const totalTests = this.tests.length;
|
|
501
537
|
debug(`Split ${totalTests} tests into ${totalChunks} chunks (max 1MB per chunk)`);
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -154,21 +154,40 @@ function CodeceptReporter(config) {
|
|
|
154
154
|
|
|
155
155
|
// mark as failed all tests inside the failed hook
|
|
156
156
|
event.dispatcher.on(event.hook.failed, hook => {
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const error = hook?.ctx?.currentTest?.err || hook?.err;
|
|
158
|
+
|
|
159
|
+
// Handle BeforeSuite and Before hooks: mark all tests in suite as failed
|
|
160
|
+
if (hook.name === 'BeforeSuiteHook' || hook.name === 'BeforeHook') {
|
|
161
|
+
const suite = hook.runnable.parent;
|
|
162
|
+
if (!suite) return;
|
|
163
|
+
|
|
164
|
+
for (const test of suite.tests) {
|
|
165
|
+
reportedTestUids.add(test.uid);
|
|
166
|
+
const reportTestPromise = client.addTestRun('failed', {
|
|
167
|
+
...stripExampleFromTitle(test.title),
|
|
168
|
+
rid: test.uid,
|
|
169
|
+
test_id: getTestomatIdFromTestTitle(test.title),
|
|
170
|
+
suite_title: stripTagsFromTitle(suite.title),
|
|
171
|
+
error,
|
|
172
|
+
time: hook?.runnable?.duration,
|
|
173
|
+
});
|
|
174
|
+
reportTestPromises.push(reportTestPromise);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
159
177
|
|
|
160
|
-
if (
|
|
178
|
+
if (hook.name === 'AfterSuiteHook') {
|
|
179
|
+
const suite = hook.runnable?.parent || hook.suite;
|
|
180
|
+
const suiteTitle = suite ? stripTagsFromTitle(suite.title) : 'Unknown Suite';
|
|
161
181
|
|
|
162
|
-
|
|
182
|
+
const stepHierarchy = buildUnifiedStepHierarchy(null, hookSteps);
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
reportedTestUids.add(test.uid);
|
|
184
|
+
// Report the hook failure as a separate test entry
|
|
166
185
|
const reportTestPromise = client.addTestRun('failed', {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
test_id: getTestomatIdFromTestTitle(test.title),
|
|
170
|
-
suite_title: stripTagsFromTitle(suite.title),
|
|
186
|
+
title: 'AfterSuite Hook',
|
|
187
|
+
suite_title: suiteTitle,
|
|
171
188
|
error,
|
|
189
|
+
message: error?.message || 'AfterSuite hook failed',
|
|
190
|
+
steps: stepHierarchy,
|
|
172
191
|
time: hook?.runnable?.duration,
|
|
173
192
|
});
|
|
174
193
|
reportTestPromises.push(reportTestPromise);
|
|
@@ -194,8 +213,10 @@ function CodeceptReporter(config) {
|
|
|
194
213
|
});
|
|
195
214
|
|
|
196
215
|
event.dispatcher.on(event.all.after, () => {
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
setImmediate(() => {
|
|
217
|
+
finalizeRun('all.after').catch(err => {
|
|
218
|
+
debug('Error finalizing run:', err);
|
|
219
|
+
});
|
|
199
220
|
});
|
|
200
221
|
});
|
|
201
222
|
|
|
@@ -278,6 +299,8 @@ async function uploadAttachments(client, attachments, messagePrefix, attachmentT
|
|
|
278
299
|
log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
|
|
279
300
|
}
|
|
280
301
|
|
|
302
|
+
await Promise.all(client.pipes?.map(pipe => pipe.addArtifacts?.(attachments)) || []);
|
|
303
|
+
|
|
281
304
|
const promises = attachments.map(async attachment => {
|
|
282
305
|
const { rid, title, path, type } = attachment;
|
|
283
306
|
const file = { path, type, title };
|
|
@@ -48,14 +48,8 @@ class PlaywrightReporter {
|
|
|
48
48
|
const { title } = test;
|
|
49
49
|
const { error, duration } = result;
|
|
50
50
|
const pwAttachments = (result.attachments || []).filter(a => a.body || a.path);
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
.map(att => ({
|
|
54
|
-
path: this.#getArtifactPath(att),
|
|
55
|
-
title: att.name || title,
|
|
56
|
-
type: att.contentType,
|
|
57
|
-
}))
|
|
58
|
-
.filter(f => f.path);
|
|
51
|
+
const files = buildArtifactFiles(pwAttachments, attachment => this.#getArtifactPath(attachment), title);
|
|
52
|
+
const artifactsForUpload = processArtifactsForUpload(pwAttachments);
|
|
59
53
|
|
|
60
54
|
const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
|
|
61
55
|
|
|
@@ -150,7 +144,7 @@ class PlaywrightReporter {
|
|
|
150
144
|
this.uploads.push({
|
|
151
145
|
rid: `${rid}-${project.name}`,
|
|
152
146
|
title: test.title,
|
|
153
|
-
files:
|
|
147
|
+
files: artifactsForUpload,
|
|
154
148
|
file: test.location?.file,
|
|
155
149
|
});
|
|
156
150
|
// remove empty uploads
|
|
@@ -229,6 +223,25 @@ function checkStatus(status) {
|
|
|
229
223
|
);
|
|
230
224
|
}
|
|
231
225
|
|
|
226
|
+
function buildArtifactFiles(attachments, getArtifactPath, title) {
|
|
227
|
+
return attachments
|
|
228
|
+
.filter(isScreenshotArtifact)
|
|
229
|
+
.map(attachment => ({
|
|
230
|
+
path: getArtifactPath(attachment),
|
|
231
|
+
title: attachment.name || title,
|
|
232
|
+
type: attachment.contentType,
|
|
233
|
+
}))
|
|
234
|
+
.filter(file => file.path);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function processArtifactsForUpload(attachments) {
|
|
238
|
+
return attachments.filter(attachment => !isScreenshotArtifact(attachment));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isScreenshotArtifact(attachment) {
|
|
242
|
+
return attachment?.contentType === 'image/png' && attachment?.name === 'screenshot';
|
|
243
|
+
}
|
|
244
|
+
|
|
232
245
|
function appendStep(step, shift = 0) {
|
|
233
246
|
// nesting too deep, ignore those steps
|
|
234
247
|
if (shift >= 10) return;
|
package/src/bin/cli.js
CHANGED
|
@@ -6,7 +6,6 @@ import { glob } from 'glob';
|
|
|
6
6
|
import createDebugMessages from 'debug';
|
|
7
7
|
import TestomatClient from '../client.js';
|
|
8
8
|
import XmlReader from '../xmlReader.js';
|
|
9
|
-
import AllureReader from '../allureReader.js';
|
|
10
9
|
import { APP_PREFIX, STATUS, DEBUG_FILE, BATCH_MODE } from '../constants.js';
|
|
11
10
|
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
12
11
|
import { config } from '../config.js';
|
|
@@ -17,6 +16,8 @@ import dotenv from 'dotenv';
|
|
|
17
16
|
import Replay from '../replay.js';
|
|
18
17
|
import { log } from '../utils/log.js';
|
|
19
18
|
import { formatFilterListIds } from '../utils/pipe_utils.js';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
20
21
|
|
|
21
22
|
const debug = createDebugMessages('@testomatio/reporter:cli');
|
|
22
23
|
const version = getPackageVersion();
|
|
@@ -353,53 +354,61 @@ program
|
|
|
353
354
|
});
|
|
354
355
|
|
|
355
356
|
program
|
|
356
|
-
.command('
|
|
357
|
-
.description('
|
|
358
|
-
.argument('
|
|
359
|
-
.option('
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
.option(
|
|
363
|
-
'--java-tests [path]',
|
|
364
|
-
'Path to test sources; links cases from @TmsLink in source for skipped tests (default: src/test)',
|
|
365
|
-
)
|
|
366
|
-
.option('--lang <lang>', 'Language used (java, kotlin, ...)')
|
|
367
|
-
.action(async (pattern, opts) => {
|
|
368
|
-
let javaTests = opts.javaTests;
|
|
369
|
-
if (javaTests === true) javaTests = 'src/test';
|
|
357
|
+
.command('upload-artifacts')
|
|
358
|
+
.description('Upload artifacts to Testomat.io')
|
|
359
|
+
.argument('[jsonl-file]', 'Path to JSONL debug file (optional)')
|
|
360
|
+
.option('--force', 'Re-upload artifacts even if they were uploaded before')
|
|
361
|
+
.action(async (jsonlFile, opts) => {
|
|
362
|
+
const apiKey = config.TESTOMATIO;
|
|
370
363
|
|
|
371
|
-
|
|
364
|
+
// JSONL file mode: upload artifacts from debug file
|
|
365
|
+
if (jsonlFile) {
|
|
366
|
+
if (!fs.existsSync(jsonlFile)) {
|
|
367
|
+
log.error(`JSONL file not found: ${jsonlFile}`);
|
|
368
|
+
return process.exit(1);
|
|
369
|
+
}
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
timeoutTimer = setTimeout(
|
|
376
|
-
() => {
|
|
377
|
-
console.log(
|
|
378
|
-
`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
|
|
379
|
-
);
|
|
380
|
-
process.exit(0);
|
|
381
|
-
},
|
|
382
|
-
parseInt(opts.timelimit, 10) * 1000,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
371
|
+
const replay = new Replay({ apiKey });
|
|
372
|
+
const { tests, runId } = replay.parseDebugFile(jsonlFile);
|
|
385
373
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
} catch (err) {
|
|
391
|
-
console.log(APP_PREFIX, 'Error uploading Allure results:', err);
|
|
392
|
-
}
|
|
374
|
+
if (!runId) {
|
|
375
|
+
log.error('runId not found in JSONL file');
|
|
376
|
+
return process.exit(1);
|
|
377
|
+
}
|
|
393
378
|
|
|
394
|
-
|
|
395
|
-
});
|
|
379
|
+
log.info(`Processing ${tests.length} tests from ${jsonlFile}`);
|
|
396
380
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
381
|
+
const client = new TestomatClient({
|
|
382
|
+
apiKey,
|
|
383
|
+
runId,
|
|
384
|
+
batchMode: BATCH_MODE.DISABLED,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await client.createRun();
|
|
388
|
+
client.uploader.checkEnabled();
|
|
389
|
+
client.uploader.disableLogStorage();
|
|
390
|
+
|
|
391
|
+
let uploadedCount = 0;
|
|
392
|
+
let failedCount = 0;
|
|
393
|
+
|
|
394
|
+
for (const test of tests) {
|
|
395
|
+
if (!hasExistingArtifacts(test)) continue;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
await client.addTestRun(undefined, { ...test, overwrite: true });
|
|
399
|
+
uploadedCount++;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
log.error(`Failed ${test.test_id || test.rid || test.title}: ${e.message}`);
|
|
402
|
+
failedCount++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
log.info(`🗄️ ${uploadedCount} tests with artifacts uploaded`);
|
|
407
|
+
if (failedCount > 0) {
|
|
408
|
+
log.warn(`⚠️ ${failedCount} tests failed to upload artifacts`);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
403
412
|
|
|
404
413
|
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
|
|
405
414
|
const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
|
|
@@ -543,6 +552,28 @@ program
|
|
|
543
552
|
}
|
|
544
553
|
});
|
|
545
554
|
|
|
555
|
+
function hasExistingArtifacts(test) {
|
|
556
|
+
const hasExistingFile = items => {
|
|
557
|
+
for (const item of items || []) {
|
|
558
|
+
const artifactPath = typeof item === 'object' ? item?.path : item;
|
|
559
|
+
if (artifactPath && fs.existsSync(artifactPath)) return true;
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
if (hasExistingFile(test.files)) return true;
|
|
565
|
+
|
|
566
|
+
const stack = [...(test.steps || [])];
|
|
567
|
+
while (stack.length) {
|
|
568
|
+
const step = stack.pop();
|
|
569
|
+
if (!step) continue;
|
|
570
|
+
if (hasExistingFile(step.artifacts)) return true;
|
|
571
|
+
if (Array.isArray(step.steps)) stack.push(...step.steps);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
546
577
|
program.parse(process.argv);
|
|
547
578
|
|
|
548
579
|
if (!process.argv.slice(2).length) {
|
package/src/client.js
CHANGED
|
@@ -159,11 +159,12 @@ class Client {
|
|
|
159
159
|
|
|
160
160
|
const uploadedArtifacts = [];
|
|
161
161
|
for (const artifact of step.artifacts) {
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
const artifactPath = typeof artifact === 'object' ? artifact?.path : artifact;
|
|
163
|
+
if (typeof artifactPath === 'string' && !isHttpUrl(artifactPath)) {
|
|
164
|
+
const filename = generateShortFilename(artifactPath);
|
|
164
165
|
try {
|
|
165
166
|
const uploadResult = await this.uploader.uploadFileByPath(
|
|
166
|
-
|
|
167
|
+
artifactPath,
|
|
167
168
|
[this.runId, testRid, 'steps', filename]
|
|
168
169
|
);
|
|
169
170
|
if (uploadResult) {
|
|
@@ -4,7 +4,6 @@ import JavaAdapter from './java.js';
|
|
|
4
4
|
import PythonAdapter from './python.js';
|
|
5
5
|
import RubyAdapter from './ruby.js';
|
|
6
6
|
import CSharpAdapter from './csharp.js';
|
|
7
|
-
import KotlinAdapter from './kotlin.js';
|
|
8
7
|
|
|
9
8
|
function AdapterFactory(lang, opts) {
|
|
10
9
|
if (lang === 'java') {
|
|
@@ -22,9 +21,6 @@ function AdapterFactory(lang, opts) {
|
|
|
22
21
|
if (lang === 'c#' || lang === 'csharp') {
|
|
23
22
|
return new CSharpAdapter(opts);
|
|
24
23
|
}
|
|
25
|
-
if (lang === 'kotlin') {
|
|
26
|
-
return new KotlinAdapter(opts);
|
|
27
|
-
}
|
|
28
24
|
|
|
29
25
|
return new Adapter(opts);
|
|
30
26
|
}
|
package/src/pipe/debug.js
CHANGED
|
@@ -111,6 +111,17 @@ export class DebugPipe {
|
|
|
111
111
|
log.info(`History: ${this.historyDir}`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Logs artifacts data to the debug file.
|
|
116
|
+
* Used for trace.zip, video files and other artifacts uploaded after tests finish.
|
|
117
|
+
* @param {Array} artifacts - Array of artifacts with { rid, title, path, type }
|
|
118
|
+
*/
|
|
119
|
+
addArtifacts(artifacts) {
|
|
120
|
+
if (!this.isEnabled || !artifacts?.length) return;
|
|
121
|
+
const logData = { action: 'addArtifacts', artifacts, runId: this.store.runId };
|
|
122
|
+
this.logToFile(logData);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
async sync() {
|
|
115
126
|
this.flushBufferedTests();
|
|
116
127
|
}
|